データベーススキーマに合わせたGo ORMを生成するSQLBoiler
この記事は Kyash Advent Calendar 2019 の16日目の記事です。 今回はKyashで利用しているSQLBoilerというツールについてご紹介します。
ORMをジェネレートするSQLBoiler
SQLBoilerはデータベーススキーマからGoのORMを生成するツールです。 データベースを読み込んで、各テーブルに対応したStructを生成します。 生成されたStructにはテーブルのCRUD操作用メソッドが生えています。 リフレクションベースのORMと違い型安全が保証されており、実行速度も優れています。 スキーマファーストなツールのため、ツールを使う前にデータベーススキーマを作成する必要があります。 SQLBoilerはスキーマ作成、ライフサイクル管理を行う機能が存在しないため、別のツールを利用する必要があります。
Installation
今回はMySQLを使用するのでMySQL用のドライバーも一緒にインストールします。
PostgreSQL、MSSQLServer 2012+、SQLite3などもサポートされており、専用のドライバーが提供されています。
go get -u -t github.com/volatiletech/sqlboiler go get -u -t github.com/volatiletech/sqlboiler/drivers/sqlboiler-mysql
今回使用するデータベースはMySQL公式のサンプルデータベースであるsakilaデータベースを使用します。
worldデータベースのほうがテーブル数が少なくサンプルに適していると思ったのですが、SQLBoilerはテーブル名とカラム名がスネークケースである必要があるためsakila を使用します。sakillaは書籍やチュートリアル用のサンプルデータベースです。
設定ファイルを定義しsqlboileコマンドを実行
まずはSQLBoiler用の設定ファイルを作成します。TOML、JSON、YAMLなどで定義可能で、環境変数も使用できます。 下記の設定はTOMLでMySQLの設定を書いたものになります。 blacklistではコード生成したくないテーブルまたはカラムを指定することが出来ます。
output = "path/to/models" [mysql] dbname = "sakila" host = "127.0.0.1" port = 3306 user = "user_name" pass = "pass" sslmode = "false" blacklist = ["migrations"]
設定ファイルが用意できたら、sqlboilerコマンドでコードを生成します。
sqlboiler --wipe mysql
コマンドの実行に成功するとoutputで指定したディレクトリにコードが出力されます。
型安全かつ可読性の高いコード
sakilaは16のテーブルが定義されていますが、その中のactorテーブルから生成されたactor.goを見ていきます。 Actor structはactorテーブルを表すstructです。
// Actor is an object representing the database table. type Actor struct { ActorID uint16 `boil:"actor_id" json:"actor_id" toml:"actor_id" yaml:"actor_id"` FirstName string `boil:"first_name" json:"first_name" toml:"first_name" yaml:"first_name"` LastName string `boil:"last_name" json:"last_name" toml:"last_name" yaml:"last_name"` LastUpdate time.Time `boil:"last_update" json:"last_update" toml:"last_update" yaml:"last_update"` R *actorR `boil:"-" json:"-" toml:"-" yaml:"-"` L actorL `boil:"-" json:"-" toml:"-" yaml:"-"` }
Actor structにはactorテーブルへの書き込み、削除、更新処理用のメソッドやHook処理用のメソッドなどが生えており、テーブルの更新処理はこのstructの責務となっています。
読取り処理(SELECT)と複数レコードの更新/削除はactorQuery structがその責務を担っています。 actorQueryはActors関数を介してアクセスします。
type ( actorQuery struct { *queries.Query } ) // Actors retrieves all the records using an executor. func Actors(mods ...qm.QueryMod) actorQuery { mods = append(mods, qm.From("`actor`")) return actorQuery{NewQuery(mods...)} }
filmテーブルを表すFilm structも見てみましょう。
ReleaseYear
フィールドの型がnull.String
になっています。
release_year
カラムは year(4) DEFAULT NULL
となっておりNULLを許容するカラムです。
nullableなフィールドを扱う際はGoのデータ型を拡張したnull
パッケージが利用されます。
null
パッケージのおかげでnullableなフィールドも安全に扱うことが可能になっています。
null
パッケージに定義されている型は全てsql.Scanner
とdriver.Valuer
を実装しているため
sql.NullXXX
の代わりにこのパッケージが利用されています。
// Film is an object representing the database table. type Film struct { FilmID uint16 `boil:"film_id" json:"film_id" toml:"film_id" yaml:"film_id"` Title string `boil:"title" json:"title" toml:"title" yaml:"title"` Description null.String `boil:"description" json:"description,omitempty" toml:"description" yaml:"description,omitempty"` ReleaseYear null.String `boil:"release_year" json:"release_year,omitempty" toml:"release_year" yaml:"release_year,omitempty"` LanguageID uint8 `boil:"language_id" json:"language_id" toml:"language_id" yaml:"language_id"` OriginalLanguageID null.Uint8 `boil:"original_language_id" json:"original_language_id,omitempty" toml:"original_language_id" yaml:"original_language_id,omitempty"` RentalDuration uint8 `boil:"rental_duration" json:"rental_duration" toml:"rental_duration" yaml:"rental_duration"` RentalRate types.Decimal `boil:"rental_rate" json:"rental_rate" toml:"rental_rate" yaml:"rental_rate"` Length null.Uint16 `boil:"length" json:"length,omitempty" toml:"length" yaml:"length,omitempty"` ReplacementCost types.Decimal `boil:"replacement_cost" json:"replacement_cost" toml:"replacement_cost" yaml:"replacement_cost"` Rating null.String `boil:"rating" json:"rating,omitempty" toml:"rating" yaml:"rating,omitempty"` SpecialFeatures null.String `boil:"special_features" json:"special_features,omitempty" toml:"special_features" yaml:"special_features,omitempty"` LastUpdate time.Time `boil:"last_update" json:"last_update" toml:"last_update" yaml:"last_update"` R *filmR `boil:"-" json:"-" toml:"-" yaml:"-"` L filmL `boil:"-" json:"-" toml:"-" yaml:"-"` }
生成されたORMを利用してコードを書いてみる
先程のActor structを利用してactorテーブルに新しくレコードを追加してみます。
SQLBoilerにはデータベースに接続する機能はないので、database/sql
パッケージを利用してデータベースに接続します。
Actor structにデータをセットしたら、Insert
メソッドを呼びだせばレコードがInsertされます。
func main() { db, err := sql.Open("mysql", "user_name:pass@tcp(127.0.0.1:3306)/sakila?parseTime=true&charset=utf8mb4") if err != nil { panic(err) } actor := &models.Actor{ FirstName: "Keanu", LastName: "Reeves", } err = actor.Insert(context.Background(), db, boil.Infer()) if err != nil { panic(err) } }
追加したレコードをSELECTしてみます。
actorQueryにアクセスするためにActors関数をコールします。
引数にはWhere句の条件となるQueryMod
を渡します。
func main() { db, err := sql.Open("mysql", "user_name:pass@tcp(127.0.0.1:3306)/sakila?parseTime=true&charset=utf8mb4") if err != nil { panic(err) } actors, err := models.Actors( models.ActorWhere.FirstName.EQ("Keanu"), models.ActorWhere.LastName.EQ("Reeves"), ).All(context.Background(), db) if err != nil { panic(err) } for _, actor := range actors { fmt.Printf("actor_id: %d, first_name: %s, last_name: %s", actor.ActorID, actor.FirstName, actor.LastName) } }
InnerJoinやEager Loadingもサポートされています。 以下はEager Loadingのサンプルです。 Filmテーブルのlanguage_idカラムは外部キー制約がかかっておりlanguageテーブルのlanguage_idを参照しています。 LoadされたLanguageはFilm structのRフィールド経由で取得できます。
func main() { db, err := sql.Open("mysql", "user_name:pass@tcp(127.0.0.1:3306)/sakila?parseTime=true&charset=utf8mb4") if err != nil { panic(err) } // 発行されたクエリが標準出力されます boil.DebugMode = true films, err := models.Films( qm.Load("Language"), ).All(context.Background(), db) if err != nil { panic(err) } for _, film := range films { fmt.Printf("%+v\n", film.R.Language) } }
qm.Load
関数は第二引数にQueryMod
を受け取れるため、QueryMod
を使ったフィルタリングも可能です。
qm.Load("Language", models.LanguageWhere.Name.EQ("English")),
自前でテンプレートを書くことで自動生成されるコードに自分のコードを追加するこも可能です。
まとめ
SQLBoilerはスキーマファーストなORMコードジェネレーターで、 生成されたコードはリフレクションベースなORMよりも可読性と実行速度に優れており操作も直感的で使いやすいです。