データベーススキーマに合わせた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、JSONYAMLなどで定義可能で、環境変数も使用できます。 下記の設定は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.Scannerdriver.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よりも可読性と実行速度に優れており操作も直感的で使いやすいです。