🌊

SeaORM + PostgreSQL で public 以外のスキーマをターゲットにマイグレーションする

2022/09/10に公開

はじめに

SeaORM とは?

SeaORM は Rust 用の ORM ライブラリです。

https://www.sea-ql.org/SeaORM/

少し前まで 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

基本的な使い方

…は省略します。
基本的にはドキュメントを読めば分かるので。

https://www.sea-ql.org/SeaORM/docs/index/

マイグレーション関連はこの辺ですね:

普通にマイグレーションしてみる

月並みな例ですが、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) によって MySchemamy_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 を探しても何も情報がないので、ライブラリのソースを読みました。

主に参考になったのはこの辺です:

途中諦めて GitHub discussions に投稿してしまったのですが、一晩寝たら解決したので自分で返信してしまいました。

https://github.com/SeaQL/sea-orm/discussions/1029

ちなみに:試してみてダメだったやり方

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 という環境変数でスキーマ名を入れようと試みたのですが、ダメでした。

参考

Discussion