Go 言語で使える ORM と SQL ビルダー

先月あたりから余暇にコツコツ作ってた脆弱性情報の収集・管理ツール jvnman の最初のリリースを行った。

jvnman についての説明は上の記事を見ていただくとして,この記事ではツールの内部で行っている SQL 文のハンドリングについて。

jvnman は収集した脆弱性情報を SQLite データベース・ファイルに格納している。 jvnman には簡単な帳票出力機能も付いているが,メインは脆弱性情報の収集・蓄積である。 一度 SQLite データベース・ファイルを作っておけば作成したデータベースのハンドリングは他のツール(たとえば Office ツール)でもできる。

(ゆえに最初は軽く考えていて spiegel-im-spiegel/go-myjvnサンプル (おまけ) として jvnman を組み込む予定だった。思ったより依存パッケージが多いことが分かってリポジトリを別にしたんだけど)

私は SQL 文を手書きで書くのは苦にならない性質なのだが(そもそも最初は A5:SQL Mk-2 みたいなツールで試して最適化を行うものだし),途中までコードを書いて流石に煩わしくなってきたので Go 言語製の ORM (Object-Relational Mapping) および SQL Builder パッケージを探すことにした。

というわけで,以下のパッケージを紹介。

go-gorp/gorp では Go 言語 の構造体(struct)を SQL のクエリ出力に関連付けることができる。 たとえば

type Vulnlist struct {
	ID          sql.NullString `db:"id,primarykey"`
	Title       sql.NullString `db:"title"`
	Description sql.NullString `db:"description"`
	URI         sql.NullString `db:"uri"`
	Creator     sql.NullString `db:"creator"`
	Impact      sql.NullString `db:"impact"`
	Solution    sql.NullString `db:"solution"`
	DatePublic  sql.NullInt64  `db:"date_public"`
	DatePublish sql.NullInt64  `db:"date_publish"`
	DateUpdate  sql.NullInt64  `db:"date_update"`
}

のように struct タグを使って関連付けるわけだ。 これで gorp.DbMap インスタンス生成時に

db, err := sql.Open("sqlite3", "./jvndb.sqlite3")
if err != nil {
    return err
}
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
dbmap.AddTableWithName(Vulnlist{}, "vulnlist")

としておけば

obj, err := dbmap.Get(Vulnlist{}, "JVNDB-2018-003082")
if err != nil {
    return err
}
if obj == nil {
    fmt.Println("mp data")
} else if ds, ok := obj.(*Vulnlist); ok {
    if ds.Title.Valid {
        fmt.Println("Title =", ds.Title.String)
    } else {
        fmt.Println("Title is NULL")
    }
}

のように書くことができる。 INSERT や UPDATE についても同じようにできる。

Masterminds/squirrel は簡易 SQL Builder で,あまりガチガチの抽象化をしないのが気に入っている。 たとえば

var ds struct {
    dateUpdate int64 `db:"date_update"`
}
if psql, args, err := squirrel.Select("date_update").From("vulnlist").Where(squirrel.Eq{"id": "JVNDB-2018-003082"}).ToSql(); err != nil {
    return err
} else if err := dbmap.SelectOne(&ds, psql, args...); err != nil {
    return err
} else {
    fmt.Println(time.Unix(ds.dateUpdate, 0))
}

のように prepared statement とパラメータをちゃんと分離してくれる。

注意しないといけないのは From() メソッドや Where() メソッドなどで連結するたびにインスタンスのコピーが発生している点だろう1jvnman のようなツールなら気にするまでもないが,短時間に大量のトランザクションが発生する可能性がある場合は注意したほうがいいかもしれない。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. Value object として見るなら正しい動きなんだけどね。 Builder ツールとしてその動きはアリなんだろうか,とつい考えてしまう。 ↩︎