Golang Cafe #41 まとめ gorpを試す。
2014/08/03に開催された「Golang Cafe #41」についてのまとめです。
かつてGolang Cafe #4ではGo言語の標準パッケージのみ(各DB用のドライバは別として)でデータベースへのアクセスを試してみました。
今回はGo言語のORMの1つであるgorpを使ってみてどれほど便利なのかを試してみました。
今回はPostgreSQLとSQLiteで確認してみましたが、以下の説明は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#のNullable
因みに、この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.Begin、Transaction.Rollback、Transaction.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という型も用意されています。