RustでDBマイグレーションの仕組みを導入(SeaORM)

Rustでマイグレーションツールを探してみると、actix等ではDieselがよく採用されているようですが、Dieselはasyncに対応していないことやmigrationの書き方があまり好みではないので別のツールを探したところ、SeaORMというORMライブラリを発見したので採用してみました。
https://www.sea-ql.org/SeaORM/

複雑なことをしていないというのもありますが、今のところ概ね困ったことも無く使えています。

# 環境

Ubuntu: 20.04
Rust: 1.59
PostgreSQL: 14.2

# インストール

```bash
$ cargo install sea-orm-cli
```

現時点では0.6.0がインストールされました。インストールするとsea-orm-cliコマンドが使えるようになります。

# セットアップ

プロジェクトのルートの.envで自分の環境にあったデータベースの設定をDATABASE_URLとして設定します。

```dot:.env
DATABASE_URL=postgres://user:pass@localhost/dbname
```

以下のコマンドを実行して、マイグレーション用のディレクトリとファイル一式を作成します。

```bash
$ sea-orm-cli migrate init
```

以下の構成でディレクトリとファイルが作成されました。

```
migration
├── Cargo.toml
├── README.md
└── src
    ├── lib.rs
    ├── m20220101_000001_create_table.rs
    └── main.rs
```

m20220101_000001_create_table.rsを量産していくことになります。

# Entityクレートの作成

マイグレーションファイル作成の前に、SeaORMではマイグレーションファイルの作成の方針として、Entityクラスを使ってコーディングする方法と使わない方法があります。
sea-orm-cliにはデータベーススキーマからEntityクラスを自動生成する機能もあるため、鶏が先か卵が先か問題になってくる気もしますが、データベーススキーマからEntityを自動生成するのは既存データベースがある場合と解釈して、今回は新しくデータベースを作成するためEntityを元にマイグレーションファイルをコーディングしていく方法で進めたいと思います。よってまずEntityクレートを作成していきます。

以下のコマンドでEntity用のクレートを作成します。

```bash
$ cargo new entity --lib
```

ディレクトリ構成は以下になります。

```
entity
├── Cargo.toml
└── src
    ├── lib.rs
    └── user.rs # 今回作成するusersテーブル用のEntityクラス
```

Cargo.tomlのdependenciesにsea-ormを足します。

```entity/Cargo.toml
[dependencies]
sea-orm = { version = "^0.6", features = [ "sqlx-postgres", "runtime-actix-rustls", "macros" ], default-features = false }
```

src/libs.rsは以下のようにしてuserを参照するようにします。

```rust:entity/src/libs.rs
pub mod user;

pub use sea_orm;
```

user.rsは以下のようなカラム構成で作成します。

```rust:entity/src/user.rs
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    #[sea_orm(column_type = "Text", nullable)]
    pub description: Option<String>,
    pub deleted: bool,
    pub created_at: DateTimeWithTimeZone,
    pub updated_at: DateTimeWithTimeZone,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        panic!("No RelationDef")
    }
}

impl ActiveModelBehavior for ActiveModel {}
```

（Entityの構造は https://www.sea-ql.org/SeaORM/docs/generate-entity/entity-structure/ ）

# マイグレーションファイルの作成の準備

マイグレーションファイルからEntityクレートが参照できるようにdependenciesを追加します。

```migration/Cargo.toml
[dependencies]
entity = { path = "../entity" }
```

# マイグレーションファイルの作成

いよいよマイグレーション用ファイルを作成します。ファイルの命名ルールはmYYYYMMDD_HHMMSS_migration_name.rsです。今回はユーザーテーブルを作るのでm20220301_150000_create_users.rsとしてみます。
（他のマイグレーションツールのようにひな形の自動生成機能が欲しい・・。）

```rust:migration/src/m20220301_150000_create_users.rs
use entity::user;
use sea_schema::migration::{
    sea_query::{self, *},
    *,
};

pub struct Migration;

impl MigrationName for Migration {
    fn name(&self) -> &str {
        "m20220301_150000_create_users"
    }
}

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                sea_query::Table::create()
                    .table(user::Entity)
                    .if_not_exists()
                    .col(ColumnDef::new(user::Column::Id).integer().not_null().auto_increment().primary_key())
                    .col(ColumnDef::new(user::Column::Name).string().not_null())
                    .col(ColumnDef::new(user::Column::Description).text())
                    .col(ColumnDef::new(user::Column::Deleted).boolean().not_null().default(false))
                    .col(ColumnDef::new(user::Column::CreatedAt).timestamp_with_time_zone().not_null())
                    .col(ColumnDef::new(user::Column::UpdatedAt).timestamp_with_time_zone().not_null())
                    .to_owned()
            ).await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(
                sea_query::Table::drop()
                    .table(user::Entity)
                    .to_owned()
            ).await
    }
}
```

使えるカラムのタイプ等は以下を参考にしました。きれいにまとまってるドキュメントとかないかな・・。
https://docs.rs/sea-schema/0.5.1/sea_schema/migration/prelude/struct.ColumnDef.html

マイグレーションファイルの基本的な作り方についてはこちらのドキュメントを参考にしました。
https://www.sea-ql.org/SeaORM/docs/migration/writing-migration

マイグレーションファイルを作成したら、libs.rsの中にも定義を追加しないといけません。

```rust:migration/src/lib.rs
pub use sea_schema::migration::*;

mod m20220301_150000_create_users;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
        vec![
            Box::new(m20220301_150000_create_users::Migration),
        ]
    }
}
```

# マイグレーションの実行

以下のコマンドでマイグレーションが実行されて最新の状態になります。

```bash
$ sea-orm-cli migrate up
```

以下のように目的のusersテーブルと管理用のseaql_migrationsテーブルが作成されました。

```
postgres=# \dt
                リレーション一覧
 スキーマ |       名前       |  タイプ  | 所有者
----------+------------------+----------+--------
 public   | seaql_migrations | テーブル | postgres
 public   | users            | テーブル | postgres
```

以下のコマンドで前回実行分のマイグレーションをロールバックできます。

```bash
$ sea-orm-cli migrate down
```

コマンドは他にもたくさん揃っているので以下を参照してください。
https://www.sea-ql.org/SeaORM/docs/migration/running-migration


色々運用してみて気づいたことがあれば追記したいと思います。

# 参照

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