goを使ったシステムにて、DBアクセス周りでgorpを使うことにしたので色々調査してDDDで言うところのrepositoryを書いた。
gorp
GitHub - go-gorp/gorp: Go Relational Persistence - an ORM-ish library for Go
gorpを選んだ理由
データ取得は「SQL文を直接書く」というところが気に入った。
(insert,updateは構造体を渡せばやってくれる。)
僕はActiveRecord的なクエリビルダがあまり好きではない。
JOIN等を含んだちょっと複雑な取得条件になると、クエリビルダで表現するにはどうすればいいのかわからず調べなければならない事が多い。
ライブラリごとに方言があり例えばActiveRecordのクエリビルダをマスターしても、Djangoではその知識は使えないので別途学ぶ必要がある。
特に[or条件]は実装が難しいのか、どのライブラリも直感的ではないインターフェースになっている感じがする。
また保守時に「最終的にどういうSQLが発行されるのか」を追わなければならないシーンも多い。
対して「SQL文を直接書けばよい」であればすぐ書けるし、どういうSQLが実行されるのかもコードからすぐわかる。
最初からSQL文を使いたいと思うようになってしまった。
DDDのrepositoryをgorpを使って書いたらこんな感じになった。
gorpの初期化関数
// gorp初期化処理 func OpenDb() (*gorp.DbMap, error) { dbUserName := "DB_USERNAME" dbPassWord := "DB_PASSWORD" dbHost := "DB_HOST" dbPort := "DB_PORT" dbDbName := "DB_DBNAME" db, err := sql.Open("mysql", fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?parseTime=true", dbUserName, dbPassWord, dbHost, dbPort, dbDbName)) if err != nil { return nil, err } dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}} dbmap.AddTableWithName(entities.User{}, "user").SetKeys(true, "Id") dbmap.CreateTablesIfNotExists() dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myApp:", log.Lmicroseconds)) return dbmap, nil }
Userエンティティ
type User struct { Id int64 `db:"id, primarykey, autoincrement"` UserName string `db:"username, notnull"` CreateTime time.Time `db:"create_time, notnull"` UpdateTime time.Time `db:"update_time, notnull"` }
repository
type UserDb struct {} // コンストラクタ func NewUserDb() *UserDb { return &UserDb{} } // User構造体を受け取ってuserテーブルに保存する func (r *UserDb) Save(user *User, executor gorp.SqlExecutor) error { user.CreateTime = time.Now() user.UpdateTime = time.Now() err := executor.Insert(user) if err != nil { return err } return nil } // idでuserデータを取得 func (r *UserDb) GetById(id int64, executor gorp.SqlExecutor) *User { user := &User{} err := executor.SelectOne(user, "select * from `user` where id = ?", id) if err != nil { return err } return user }
保存処理
func someFunc() error { db, err := OpenDb() if err != nil { return err // 成否チェック } defer db.Db.Close() // 関数終了時にClose処理するために記載 userRepository := NewUserDb() user1 := &User{UserName: "テスト太郎"} err = userRepository.Save(user1, db) // dbコネクションを渡して保存。ここはトランザクションの範囲外 if err != nil { return err // 成否チェック } tx, err := db.Begin() // トランザクション開始 if err != nil { return err // 成否チェック } defer tx.Rollback() // コミットされていれば実行されても何もされない(内部的には「既にトランザクションが閉じています」的なエラーがでてる)。コミットされてなければRollbackされる。 user2 := &User{UserName: "テスト次郎"} err = userRepository.Save(user2, tx) // トランザクションを渡して保存 if err != nil { return err } tx.Commit() // 問題なければコミット return nil }
気になった点
トランザクション分離レベルが指定できない
gorpはトランザクション開始時にトランザクション分離レベルを指定することができない。
DB側のデフォルト設定が適用されることになる。
※内部的には設定を渡せる部分があるが、かならずnilが渡されている。
楽観的同時実行制御の機能があるので問題ないのかもしれない。
https://github.com/go-gorp/gorp#optimistic-locking
rollbackの書き方
gorpというよりgo言語の問題かもしれないが、例外機能が無いので「例外をキャッチしたらrollback」というような書き方ができない。
色々悩んだ結果、上記「保存処理」のコードのように
tx, err := db.Begin() // トランザクション開始 if err != nil { return err // 成否チェック } defer tx.Rollback() // コミットされていれば実行されても何もされない(内部的には「既にトランザクションが閉じています」的なエラーがでてる)。コミットされてなければRollbackされる。
と書いた。
gorpのRollback()はトランザクションがcloseしているとsql.ErrTxDoneを返してくるだけで何もしない。
だから、Commitされていなければ(途中でエラーが出たとか、Commitし忘れていたとか)Rollbackされるし、
Commitされていればsql.ErrTxDoneを返して何もされない。
以下疑問が浮かんだが問題なさそうだった。
・ 「Rollbackが失敗していた場合どうなる?」
Rollbackが失敗したところで、Commitしていなければ変更は反映されないので問題なさそう。
※となると明示的にRollback処理書かなくてもいいのかな。。?
・ 「Rollbackが失敗し、次のトランザクションが開始されCommitされた場合、RollbackされるべきデータがDBへ反映されてしまわないか?」
使うトランザクションオブジェクトが異なるので影響しない。問題なさそう。