SeaORM + PostgreSQL で public 以外のスキーマをターゲットにマイグレーションする
はじめに
SeaORM とは?
SeaORM は Rust 用の ORM ライブラリです。
少し前まで Rust の ORM といえば Diesel がデファクトスタンダードだったのですが、非同期処理対応の関係で、少し選びにくい状況となっていました。
代わりに SQLx を使ったりしていたのですが、こちらは生 SQL を実行するに留まるシンプルなもので、あくまで ORM を使いたい人には物足りないのではないかと思います。
そんな中出てきたのがこの SeaORM です。
SeaORM の何がいいの?
非同期処理への対応が手厚い
SeaORM は先述の SQLx をバックに使っており、その恩恵として非同期処理に対応しています。
Diesel 最大のネックでもあったのですが、そもそも Rust の非同期周りの対応はゴタゴタしていて、今は主流となっている Future
や async/await も、割と最近になって(安定版として)導入されたものです。
SQLx/SeaORM は最初から非同期処理を念頭において開発されたという経緯もあり、手厚くサポートしてくれています。
現在 Rust の非同期ランタイムとして主流な actix
async-std
tokio
のすべてに対応しており、状況に応じてどれを使うか選ぶことができます。
機能感がちょうどいい(※個人の感想です)
以前使っていた Diesel は色々とリッチで、スキーマ定義マクロからマイグレーション結果・マッパー構造体までを一気通貫で静的に型検査したりできたのですが、他方 DB 接続がコケたせいでビルドもコケるみたいな煩わしさもあり(回避方法があったかも?)、個人的にはちょっと「やりすぎ」な感触でした。
SeaORM は、言ってみれば「よくある ORM」に近い感じです。
言い換えれば、ある意味そんなに大きな特徴がないとも言えそうです。
Active Record や SQLAlchemy とかに慣れた人でも、馴染みやすい…?
また、マッパーだけ必要でマイグレーションは不要、みたいな場面でも使えるので、すでに稼働している DB に対してアプリを追加するのも容易です。
バージョン情報等
本記事の試行で使ったランタイムやライブラリのバージョンです。
- Rust
1.63.0
- sea-orm
0.9.2
- sea-orm-migration
0.9.2
基本的な使い方
…は省略します。
基本的にはドキュメントを読めば分かるので。
マイグレーション関連はこの辺ですね:
- Setting Up Migration | SeaORM 🐚 An async & dynamic ORM for Rust
- Writing Migration | SeaORM 🐚 An async & dynamic ORM for Rust
- Running Migration | SeaORM 🐚 An async & dynamic ORM for Rust
普通にマイグレーションしてみる
月並みな例ですが、id
name
列を持つ users
というテーブルを作ってみます。
CLI で生成した雛形をベースに、マイグレーション定義を作っていきます。
$ sea-orm-cli migrate generate create_users
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Users::Name).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(Iden)]
enum Users {
Table,
Id,
Name,
}
これを適用します。
$ sea-orm-cli migrate up
すると、デフォルトの public
スキーマに users
テーブルが作られます。
# \dt
List of relations
Schema | Name | Type | Owner
--------+------------------+-------+-------
public | seaql_migrations | table | admin
public | users | table | admin
(2 rows)
public
以外のスキーマにテーブルを作る
本題:以下、my_schema
という名前のスキーマに、先述の users
テーブルを作ります。
基本方針
up/down でテーブル名を指定している箇所に対して、({スキーマ}, {テーブル})
という形のタプルを渡してやれば、そのスキーマの下にテーブルを作ってくれます。
- before
.table(Users::Table)
- after
.table((Users::Schema, Users::Table))
一番シンプルなやり方
derive(Iden)
の恩恵を受けるやり方です。
テーブル定義を詰め込む enum に、スキーマ名をキャメルケースで突っ込みます。
#[derive(Iden)]
enum Users {
MySchema, // <- これ
Table,
Id,
Name,
}
こうすると、derive(Iden)
によって MySchema
が my_schema
と解決されるようになります。
で、基本方針に従い、.table((Users::MySchema, Users::Table))
のようにします。
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table((Users::MySchema, Users::Table)) // <- これ
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Users::Name).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table((Users::MySchema, Users::Table)) // <- これ
.to_owned(),
)
.await
}
}
これで実行すると、my_schema.users
が作成されます。やったね!
# \dt my_schema.*
List of relations
Schema | Name | Type | Owner
-----------+-------+-------+-------
my_schema | users | table | admin
(1 rows)
別のやり方
テーブルは Table
=> enum
名と解決しているのに、スキーマは具体的な名前を enum
に入れるのは、なんか気持ち悪く感じる方もいらっしゃると思います。
そういうときは、ちょっと面倒ですが、Iden
トレイトを手動で実装します。
※好きで Rust 書いてる人はこの程度面倒がらない印象ではあります。
enum Users {
Schema,
Table,
Id,
Name,
}
impl Iden for Users {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Schema => "my_schema",
Self::Table => "users",
Self::Id => "id",
Self::Name => "name",
}
)
.unwrap();
}
}
先ほど Users::MySchema
を使っていた箇所は、Users::Schema
に置き換えます。
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
##
.table((Users::Schema, Users::Table))
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Users::Name).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table((Users::Schema, Users::Table))
.to_owned(),
)
.await
}
}
これで実行すると、同様に my_schema.users
が作成されます。
どっちのやり方がいいの?
今のところ、公式にやり方の記載もないですし、「好み」という印象です。
私は「別のやり方」の方が好みです。
ちなみに:このやり方にたどり着いた経緯
公式ドキュメントや GitHub の Issue/discussions を探しても何も情報がないので、ライブラリのソースを読みました。
主に参考になったのはこの辺です:
- https://github.com/SeaQL/sea-query/blob/cfaa483ff060051241c495a822cb05cccfd95bcd/src/table/create.rs#L133
- https://github.com/SeaQL/sea-query/blob/cfaa483ff060051241c495a822cb05cccfd95bcd/src/types.rs#L321
途中諦めて GitHub discussions に投稿してしまったのですが、一晩寝たら解決したので自分で返信してしまいました。
ちなみに:試してみてダメだったやり方
my_schema.users
というテーブル名で作る
Iden
を手動実装し、Self::Table => "my_schema.users"
のように解決させるやり方です。
結果、public
スキーマに my_schema.users
というテーブルが出来上がりました。(.
入りのテーブルなんて作れるんだ…)
enum Users {
Table,
Id,
Name,
}
impl Iden for Users {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "my_schema.users",
Self::Id => "id",
Self::Name => "name",
}
)
.unwrap();
}
}
sea-orm-cli
のオプションでなんとかする
なんとかなりませんでした。
エンティティファイルを生成するコマンドでは、スキーマを入れられるようです。
これを真似て -s
/ --database-schema
というオプションや DATABASE_SCHEMA
という環境変数でスキーマ名を入れようと試みたのですが、ダメでした。
参考
- SeaORM 🐚 An async & dynamic ORM for Rust
- Async Programming | SeaORM 🐚 An async & dynamic ORM for Rust
- Diesel is a Safe, Extensible ORM and Query Builder for Rust
- launchbadge/sqlx: 🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, SQLite, and MSSQL.
Discussion