こんにちは、開発チーム サーバサイド担当の由利です。
こちらの記事は、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は主に次のようなものが利用されています。
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のこと という記事が公開される予定です。こちらもご覧いただけると幸いです。
最後まで読んでいただきありがとうございました!