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の初期化関数
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{}
}
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
}
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()
userRepository := NewUserDb()
user1 := &User{UserName: "テスト太郎"}
err = userRepository.Save(user1, db)
if err != nil {
return err
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.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()
と書いた。
gorpのRollback()はトランザクションがcloseしているとsql.ErrTxDoneを返してくるだけで何もしない。
だから、Commitされていなければ(途中でエラーが出たとか、Commitし忘れていたとか)Rollbackされるし、
Commitされていればsql.ErrTxDoneを返して何もされない。
以下疑問が浮かんだが問題なさそうだった。
・ 「Rollbackが失敗していた場合どうなる?」
Rollbackが失敗したところで、Commitしていなければ変更は反映されないので問題なさそう。
※となると明示的にRollback処理書かなくてもいいのかな。。?
・ 「Rollbackが失敗し、次のトランザクションが開始されCommitされた場合、RollbackされるべきデータがDBへ反映されてしまわないか?」
使うトランザクションオブジェクトが異なるので影響しない。問題なさそう。