【AI Shift Advent Calendar 2022】SQLBoilerでMulti Schemaへのアクセスを試してみる

こんにちは、開発チーム サーバサイド担当の由利です。
こちらの記事は、AI Shift Advent Calendar 18日目の記事になります。

今回はGo言語サーバサイド実装関連の記事です。
ORM(O/R Mapper)の1つ、SQLBoilerを使ってMulti SchemaのDBへのアクセスを試してみたいと思います。

はじめに

通常のWebアプリケーションであれば、接続先のDB(Schema)は主として1つで、複数Schemaを同時に切り替えて使うという場面はないかと思います。

ただ、例えばB2Bアプリケーションで複数の顧客に提供するサービス(いわゆるマルチテナントサービス)の場合、データマネジメントの観点などから複数Schemaに分割する。というケースはあり得ます。

また、他にも下記のようなケースが考えられそうです。

  • マルチテナントサービス
  • レポート作成など、複数サービスのDBを連結するようなアプリケーション
  • 水平シャーディング
    ※シャーディングの場合は大抵性能要件によるものなので、DBインスタンス自体を分割されるとは思いますが...

今回はこういったケースにSQLBoilerでどう対応するかについて試してみたいと思います。

SQLBoilerとは?

本題に入る前に「SQLBoiler」についてご紹介します。

Go言語ではORMは主に次のようなものが利用されています。

ORMGitHub Star数Migration機能
GORM30.8k
ent12.6k
sqlx13k-
SQLBoiler5.6k-
※2022/12/16 現在

SQLBoiler はコードから DB のテーブルを生成するのではなく、あらかじめ作成済みの DB のテーブルからコードを生成します。
そのため、他のマイグレーションツール等と組み合わせて使うことが想定されています。

筆者の場合、データ設計をしてからアプリケーション開発に取り掛かることが多く、大抵の場合マイグレーションが先行していることや、機能改修の際にModel生成しなおすことで影響範囲の修正をスピーディに実施できることなどから、SQLBoilerを好んで利用しています。

SQLBoilerでMulti SchemaのDBからデータを取得する

サンプルコード

今回実装したサンプルコードは下記に上げています。
https://github.com/ayuri-kn/sqlboiler-sample

この記事は上記リポジトリを前提としています。あわせてご参照ください!

前提環境

前提としている環境は下記です。
※Windowsでも動作すると思うのですが未確認です。ご了承ください。

  • macOS 12.5.1
  • Docker環境(Docker Desktopなど)

まずはSimpleにSQLBoilerを利用してみる

早速SQLBoilerを使ってデータ取得を体験してみましょう。

今回DBはPostgreSQLを使用します。
下記テーブルとサンプルデータが入っています。
同一テーブルがschema1,schema2それぞれに作成してあります。
それぞれのSchemaには別々のサンプルデータを入れておきました。

CREATE TABLE book (
  id integer, 
  name varchar(255),
  PRIMARY KEY (id)
);

リポジトリをcloneし、docker-composeにてDBとSQLBoilerが利用できるコンテナを起動します。
コンテナを起動したら中に入ります。以降はコンテナ内で操作します。

$ docker-compose up -d
$ docker exec -it sqlboiler /bin/sh

直下にある sqlboiler.toml を確認してみてください。
SQLBoilerは先述の通りDBからコード生成して利用するタイプのORMです。
コード生成するためのDB接続先を sqlboiler.toml に記載しておきます。

[psql]
  dbname = "db"
  host   = "sqlboiler-postgres"
  port   = 5432
  user   = "usr"
  pass   = "password"
  sslmode = "disable"

では、早速コード生成してみます。
既にSQLBoilerはinstall済みなので、下記コマンドを実行してみましょう。

$ sqlboiler psql --output models --wipe

これでmodels配下にコードが生成されました。
これを使ってDBからデータ取得してみましょう。
データ取得のためのコードは下記のように作っています。
下記は、schema1に接続してデータを取得するようにしています。

package main

import (
    "context"
    "database/sql"
    "fmt"

    "sqlboiler-sample/models"

    _ "github.com/lib/pq"
)

func main() {
    db, _ := sql.Open(
        "postgres",
        "host=sqlboiler-postgres dbname=db user=usr password=password search_path=schema1 sslmode=disable")

  books, _ := models.Books().All(context.Background(), db)
    for i, book := range books {
        fmt.Printf("book[%d]:%v\n", i, book)
    }
}

実行してみると、schema1に入っているデータを取得出来ました!

$ go run cmd/fixed_schema.go

book[0]:&{1 {Alice's Adventures in Wonderland true} <nil> {}}
book[1]:&{2 {THE COMPLETE WORKS OF ERNEST HEMINGWAY true} <nil> {}}
book[2]:&{3 {Off on a Comet true} <nil> {}}
book[3]:&{4 {THE OLD MAN AND THE SEA true} <nil> {}}
book[4]:&{5 {The Alchemist true} <nil> {}}

Schemaを切り替えながら、両方のSchemaの情報を取得する

では、次にschema1,schema2のそれぞれからデータを取得してみます。

SQLBoilerは、公式では異なるSchemaからデータを取得する場合はそれぞれのschemaからmodelを生成することを前提としています。
https://github.com/volatiletech/sqlboiler#how-should-i-handle-multiple-schemas
これは、通常Schemaごとに別テーブル(=別Model)になりうるからと思われます。

ただ、異なるSchemaに同一テーブル群が入っているケースも多々あると思いますので、今回はその前提で進めてみたいと思います。

今回は異なるSchemaへのアクセスを切り替えることができるように、生成されるModelに関数を追加するアプローチで試してみました。

PostgreSQLはtable prefixとしてschemaを指定してSQLを実行できるので、例えばこういった関数があれば、schemaを指定してデータを取得することができます。

func FindBookWithSchema(ctx context.Context, exec boil.ContextExecutor, targetSchema string, iD int, selectCols ...string) (*Book, error) {

    ...

    query := fmt.Sprintf(
        "select %s from \"%s\".book where \"id\"=$1", sel, targetSchema,
    )

    ...

これを実現するためにSQLBoilerのtemplate機能を利用します。
template定義の詳細については、GitHubリポジトリを参照してください。
https://github.com/ayuri-kn/sqlboiler-sample/tree/main/templates

sqlboiler.tomlには、追加したtemplateを利用するように定義を追加しておきます。

templates = [
  "${GOPATH}/pkg/mod/github.com/volatiletech/sqlboiler/v4@<sqlboiler-version>/templates/main", # sqlboilerのテンプレートディレクトリ
  "${GOPATH}/pkg/mod/github.com/volatiletech/sqlboiler/v4@<sqlboiler-version>/templates/test", # sqlboilerのテスト用テンプレートディレクトリ
  "templates" # カスタムテンプレートディレクトリ
]

[psql]
  dbname = "db"
  host   = "sqlboiler-postgres"
  port   = 5432
  user   = "usr"
  pass   = "password"
  sslmode = "disable"

これで準備は整いました。再度コード生成を実行します。

$ sqlboiler psql --output models --wipe

生成したコードを利用して、schema切り替えを試してみます。
下記のように実装し、実行してみましょう。

package main

...

func main() {
    db, _ := sql.Open(
        "postgres",
        "host=sqlboiler-postgres dbname=db user=usr password=password search_path=schema1 sslmode=disable")

    schemas := []string{"schema1", "schema2"}
    for _, schema := range schemas {
        // select * from ... のようにデータを取得する
        books, _ := models.BooksWithSchema(schema).All(context.Background(), db)
        for i, book := range books {
            fmt.Printf("[findAll] %s.book[%d]:%v\n", schema, i, book)
        }
    }

    for _, schema := range schemas {
        // select * from ... where id = ... のようにPK指定でデータ取得する
        book, _ := models.FindBookWithSchema(context.Background(), db, schema, 1)
        fmt.Printf("[find] %s.book[1]:%v\n", schema, book)
    }
}

実行結果は下記の通りです。
schemaを切り替えながらデータ取得することが出来ました!

$ go run cmd/multi_schema.go

[findAll] schema1.book[0]:&{1 {Alice's Adventures in Wonderland true} <nil> {}}
[findAll] schema1.book[1]:&{2 {THE COMPLETE WORKS OF ERNEST HEMINGWAY true} <nil> {}}
[findAll] schema1.book[2]:&{3 {Off on a Comet true} <nil> {}}
[findAll] schema1.book[3]:&{4 {THE OLD MAN AND THE SEA true} <nil> {}}
[findAll] schema1.book[4]:&{5 {The Alchemist true} <nil> {}}
[findAll] schema2.book[0]:&{1 {The Merry-Go-Round true} <nil> {}}
[findAll] schema2.book[1]:&{2 {The Book of Marvels and Travels true} <nil> {}}
[findAll] schema2.book[2]:&{3 {Norse Mythology true} <nil> {}}
[findAll] schema2.book[3]:&{4 {Grimm's Complete Fairy Tales true} <nil> {}}
[findAll] schema2.book[4]:&{5 {The Wonderful Wizard of Oz true} <nil> {}}
[find] schema1.book[1]:&{1 {Alice's Adventures in Wonderland true} <nil> {}}
[find] schema2.book[1]:&{1 {The Merry-Go-Round true} <nil> {}}

課題

と、ここまで読まれた方はお気づきとは思いますが、template指定の中に下記のようにsqlboilerのversion指定がありました。

templates = [
  "${GOPATH}/pkg/mod/github.com/volatiletech/sqlboiler/v4@<sqlboiler-version>/templates/main", # sqlboilerのテンプレートディレクトリ
  "${GOPATH}/pkg/mod/github.com/volatiletech/sqlboiler/v4@<sqlboiler-version>/templates/test", # sqlboilerのテスト用テンプレートディレクトリ
  "templates" # カスタムテンプレートディレクトリ
]

SQLBoiler自身のテンプレートも含める必要があり、これはバージョンアップなどの際に問題になりそうです。

また、SQLBoiler自身のテンプレートを指定した場合、そのテンプレートのoverrideをすることが出来ませんでした。
(試してみた限りでの話ですので、もし可能な方法をご存知の方がいらっしゃいましたら、ぜひお知らせくださると嬉しいです!)

他にも下記のような課題がありそうです。

  • Schema切り替えタイプではない関数を誤って使ってしまう可能性がある
    ※SQLBoiler自身のデフォルトテンプレートを残していた場合
  • 基本的に全ての関数でSchema切り替え処理を追加する必要がある
  • 利用可能なDBが限られる

そもそもSchemaごとに接続を切り替えてアクセスするべきでは?という正攻法は常に自分の胸の内からも聞こえて来るので、 こんな方法もあるのかな。
程度にとどめて頂けたらと思います...。

おわりに

今回の記事では、SQLBoilerを使ってMulti SchemaのDBへのアクセスについて考察してみました。
再掲になりますが、GitHubにsampleを用意してありますのでもしよければお試しください。
https://github.com/ayuri-kn/sqlboiler-sample

今回利用したSQLBoilerというORMは「DBに接続してModelを生成する」部分の手間を除けば、かなり使い勝手の良いORMではないかと思います。
ぜひ、検討の選択肢に加えてみてください!

明日はAIチームの柾屋から データアナリストになるためにやった10のこと という記事が公開される予定です。こちらもご覧いただけると幸いです。

最後まで読んでいただきありがとうございました!

参考

PICK UP

TAG