のんびり精進

調べた情報などをおすそ分けできれば。

【Go】gorp を使って DB 操作を少し楽にする

github.com


gorp の気に入ったところ

ORM は便利ですが、私は SQL 文を書いてパフォーマンスを調整したいので、ORM のためのパッケージだと自分の使い方に合いません。

その点で gorp は合格でした。
"an ORM-ish library" とのことで、がっつり ORM ではないようです。
Go 標準の database/sql パッケージより少し便利にしたものという感覚で使えそうだと思いました。


お試し用のテーブルを用意

使い慣れていて環境を用意しやすかったので MySQL を使いました。

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  `age` tinyint(3) unsigned NOT NULL,
  PRIMARY KEY (`id`)
);

ユーザ情報のテーブルという想定です。 次のようにデータを入れておきます。

id name age
1 太郎 34
2 次郎 32
3 三平 30
4 四郎 28

すみませんが、Go のコードによるテーブル作成やデータ追加は今回説明しません。 手を抜いて MySQL Workbench で済ませたので…。


SELECT を試す

id が 1 と 3 のレコードを取得してみます。

database/sql をそのまま使う場合
package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "log"
    "fmt"
)

func main() {
    db, err := sql.Open("mysql", "user:pw@tcp(host:3306)/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    var (
        id   uint32
        name string
    )

    rows, err := db.Query(`SELECT id, name FROM user WHERE id IN (1, 3)`)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        err := rows.Scan(&id, &name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("id: %d, name: %s\n", id, name)
    }

    err = rows.Err()
    if err != nil {
        log.Fatal(err)
    }
}

database/sql はなんだかやらなきゃいけないことが多いですね。毎回こんな風に書くことになるならウンザリしそうです。 特に ↓ この部分。

err := rows.Scan(&id, &name)

変数名を取得対象カラムと同じにしているのに、Scan() でちゃんと指定しないといけないなんて…。

しかも、この引数を SELECT 文での指定と異なる順にするとエラーになります。

rows, err := db.Query(`SELECT id, name FROM user`)
// (中略)
err := rows.Scan(&name, &id)
2017/09/17 01:23:45 sql: Scan error on column index 1: converting driver.Value type []uint8 ("太郎") to a uint32: invalid syntax
gorp の場合
package main

import (
    "database/sql"
    "fmt"
    "github.com/go-gorp/gorp"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

type user struct {
    Id        uint32 `db:"id"`
    FirstName string `db:"name"` // FirstName に name というカラムを紐付ける
}

func main() {
    db, err := sql.Open("mysql", "user:pw@tcp(host:3306)/dbname")
    if err != nil {
        log.Fatal(err)
    }

    dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{}}
    defer dbmap.Db.Close()

    rows, err := dbmap.Select(user{}, `SELECT id, name FROM user WHERE id IN (1, 3)`)
    if err != nil {
        log.Fatalln(err)
    }
    for i, r := range rows {
        row := r.(*user)
        fmt.Printf("[%d] id: %d, name: %s\n", i, row.Id, row.FirstName)
    }
}

先ほどより少し短く書けました。 マッパー(DbMap)のところが増えていますが、SELECT 文以降はとてもシンプルになりましたね。

構造体を用意して db:"カラム名" のように書くことでテーブルのカラムと紐付けることができます。それを dbmap.Select() で指定すれば、データをその構造体の構造で取り出せます。

なお、紐付けは次のように行うこともできます。

table := dbmap.AddTable(user{})
table.ColMap("Id").Rename("id")
table.ColMap("FirstName").Rename("name")

ちなみに、構造体名とテーブル名が異なる場合は AddTableWithName() が使えます。 構造体が myuser、テーブルが user なら次のようになります。

table := dbmap.AddTableWithName(myuser{}, "user")

取得カラム数と入れ物の数が合わない場合

取得したデータを入れる変数や構造体のフィールドを上記のように2つしか用意していないまま、テーブルが持つ3カラム全てを取得しようとしたらどうなるでしょうか。

database/sql
rows, err := db.Query(`SELECT * FROM user`)
// (中略)
err := rows.Scan(&id, &name)
2017/09/17 01:23:45 sql: expected 3 destination arguments in Scan, not 2
gorp
type user struct {
    Id        uint32 `db:"id"`
    FirstName string `db:"name"` // name というカラムを FirstName に割り当てる
}
rows, err := dbmap.Select(user{}, `SELECT * FROM user`)
2017/09/17 01:23:45 gorp: no fields [age] in type user

全カラムを取得対象にしておいて後で欲しいカラムだけを取り出そうなんて、都合の良すぎる話なんですね。 PHP ではできますが。

エラーは gorp のほうがわかりやすいです。 足りないカラム名まで教えてくれています。


まとめ

今回は基本的な挙動を理解しやすい程度にまとめてみました。 database/sql は使いやすいとは言えませんが、gorp を使えば随分と楽に書くことができ、実用できそうだと感じました。

それでも ORM-ish でない本物の ORM が必要な人にとっては不十分かもしれません。 少し使って試すといいでしょう。

次回は、より実践で使えそうなプレースホルダを見てみたいと思います。

⇒ 書きました。 kabochapo.hateblo.jp