Golang Cafe #41 まとめ gorpを試す。

2014/08/03に開催された「Golang Cafe #41」についてのまとめです。

かつてGolang Cafe #4ではGo言語の標準パッケージのみ(各DB用のドライバは別として)でデータベースへのアクセスを試してみました。
今回はGo言語のORMの1つであるgorpを使ってみてどれほど便利なのかを試してみました。


今回はPostgreSQLSQLiteで確認してみましたが、以下の説明はSQLiteでの結果を中心にまとめておきます。
検証に使用したコードはmattnさんのコードをベースにしました。

準備

以下の2つのパッケージをインストールします。

$ go get github.com/coopernurse/gorp
$ go get github.com/mattn/go-sqlite3

※mattn/go-sqlite3をインストールするにはgccが必要になるため、Windows環境の方は事前にgccをインストールしておく必要があります。(gccのインストールについては「Windows7 64bit版でGo言語のクロスコンパイルを試す」を参考にしてください)

検証

テーブルに対応した構造体を定義しておきます。

type Person struct {
    Id       int32
    Name     sql.NullString
    Age      sql.NullInt64
    Sex      sql.NullBool
    Height   sql.NullFloat64
    Birthday time.Time
    BString  []byte
    BBigInt  []byte
}

項目の型ですが、bool、、float64、int64、string、[]byte、time.Time型、Null許容型としてNullBool,、NullFloat64,、NullInt64、NullString、nilにしか対応していません。(Go言語の標準がそうだったと思います)
またDB上ではnullとして扱えても、Go言語ではNull許容型(C#のNullableJavaのInteger)というのが存在しないので注意が必要です。
因みに、このPerson型をjson.Marshalしても、NullString(ほか)の構造体まで展開されてしまうので気をつけなくてはいけません。


次に実際にデータベースにアクセスする処理を書いていきます。

func main() {
    db, err := sql.Open("sqlite3", "./foo.db")
    if err != nil {
        panic(err.Error())
    }

    dbmap := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
    t := dbmap.AddTableWithName(Person{}, "person").SetKeys(true, "Id")
    t.ColMap("Id").Rename("id")
    t.ColMap("Name").Rename("name")
    t.ColMap("Age").Rename("age")
    t.ColMap("Sex").Rename("sex")
    t.ColMap("Height").Rename("height")
    t.ColMap("Birthday").Rename("birthday")
    t.ColMap("BString").Rename("bstring")
    t.ColMap("BBigInt").Rename("bbigint")
    dbmap.DropTables()
    err = dbmap.CreateTables()
    if err != nil {
        panic(err.Error())
    }

    tx, _ := dbmap.Begin()
    for i := 0; i < 100; i++ {

        p := Person{}
        if rand.Float32() > 0.5 {
            p.Name.Scan(fmt.Sprintf("mattn%03d", i))
        }

        if rand.Float32() > 0.5 {
            p.Age.Scan(i)
        }

        if rand.Float32() > 0.5 {
            p.Sex.Scan(rand.Float32() > 0.5)
        }

        if rand.Float32() > 0.5 {
            p.Height.Scan(rand.Float32())
        }

        if rand.Float32() > 0.5 {
            p.Birthday = time.Now()
        }

        if rand.Float32() > 0.5 {
            p.BString = []byte(fmt.Sprintf("mattn%03d", i))
        }

        if rand.Float32() > 0.5 {
            p.BBigInt = big.NewInt(int64(i * 10000)).Bytes()
        }

        err = tx.Insert(&p)
        if err != nil {
            tx.Rollback()
            panic(err.Error())
        }
    }
    tx.Commit()

    list, _ := dbmap.Select(&Person{}, "select * from person")
    for _, l := range list {
        p := l.(*Person)

        var (
            bstring string
            bbigint big.Int
        )

        if p.BString != nil {
            bstring = string(p.BString)
        }

        if p.BBigInt != nil {
            bbigint = big.Int{}
            bbigint.SetBytes(p.BBigInt)
        }

        fmt.Printf("%d, %s, %d, %t, %f, %v, %d\n", p.Id, p.Name, p.Age, p.Sex, p.Height, bstring, bbigint)
    }
}

簡単に流れを説明すると、まずgorp.DbMap型を作成します。作成するときに各データベース用のDialectを指定します。
dbmap.AddTableWithNameメソッドでテーブル名と対応する構造体を指定します。またSetKeysメソッドで主キーを指定します。
その後でTableMap.ColMapメソッドとColumnMap.Renameメソッドで構造体の項目とテーブルの項目を結びつけてあげます。
DbMap.DropTablesメソッドとDbMap.CreateTablesはそのままテーブルの削除と作成です。このサンプルではDbMap.DropTablesの戻り値は無視しているのですが、厳密にテーブルが存在している場合のみテーブルを削除する場合は、DbMap.DropTablesIfExistsメソッドを使用します。


DbMap.BeginTransaction.RollbackTransaction.Commitトランザクションを使用することができます。ドキュメントを見る限りではセーブポイントも利用できそうです。


クエリーの発行については、Transactionのメソッドを実行するか、DbMapのメソッドを実行するかのいずれかになります。


実際の実行結果は以下のようになります。

1, {mattn000 %!s(bool=true)}, {0 %!d(bool=true)}, {false true}, {0.000000 %!f(bool=false)}, , {%!d(bool=false) []}
2, { %!s(bool=false)}, {0 %!d(bool=false)}, {true true}, {0.000000 %!f(bool=false)}, , {%!d(bool=false) []}
3, { %!s(bool=false)}, {0 %!d(bool=false)}, {false true}, {0.000000 %!f(bool=false)}, mattn002, {%!d(bool=false) [20000]}
4, { %!s(bool=false)}, {0 %!d(bool=false)}, {false true}, {0.696719 %!f(bool=true)}, , {%!d(bool=false) []}
5, {mattn004 %!s(bool=true)}, {4 %!d(bool=true)}, {false false}, {0.059121 %!f(bool=true)}, , {%!d(bool=false) []}
...

非常に見難いのはNullInt64のような型を使用しているためです。


GUIクライアントで確認してみるとこのようになっています。


また作成されたテーブルは以下のこのようになっています。


確かに簡単にテーブルを作成してくれるのは嬉しいですが、DBの設計に拘る人にとっては思うようなテーブルを作成することは難しそうに思います。(できるのかもしれませんがそこまでは確かめていません)
それに関連してDecimal型を扱うのにはどうしたら良いのかというのが気になります。Go言語ではC#のようなdecimal型はなくmath/bigパッケージのInt型を使うしかありません。
不運なことにbig.Int型Go言語の標準あるいはDBのドライバでも対応してなさそなので、苦肉の策として
BOLB型として保存してみました。ただBLOB型では保存しているだけなのであまり意味がなさそうな気がします。
一日も早いdecimal型への対応が望まれます!

PostgreSQL

ちなみにPostgreSQLを使用する場合は、以下のように2箇所を変更するだけで動作します。

db, err := sql.Open("postgres", "user=postgres password=postgres host=localhost dbname=godbtest sslmode=disable")

dbmap := gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}

またpgドライバにはpq.NullTimeという型も用意されています。

まとめ

以前、ORMを使用しないで直接DBに接続したこと思えば遥かに便利にはなっていますが、さまざまな要件にそってDB設計されたものに対して使用するのは、まだまだどうなのかなという感じはしています。
最近ではGo言語でシステム開発される話を耳にしたりしますが、その辺りのギャップをどのように吸収しているのか非常に気になっています。