🏄‍♂️

Rust | SQLx 上に構築された ORM "SeaORM" を試してみた

2023/09/27に公開

ソースコードは Github でも確認できます。

https://github.com/codemountains/sea-orm-postgresql-demo/tree/main

SeaORM とは

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

SeaORM は Rust と各種 RDB を接続するためのクレート(ライブラリ)です。

多くの DB エンジンと遅延読み込みをサポートしています。

また、SQLx 上に構築された ORM であることも特徴のひとつです。

Migration

sea-orm-cli をインストール

cargo install sea-orm-cli

セットアップ

マイグレーションのためのセットアップをおこないます。

以下のコマンドを実行すると、migration というフォルダが作成されます。

sea-orm-cli migrate init
.
├── Cargo.lock
├── Cargo.toml
├── migration
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       └── main.rs
└── src
    └── lib.rs

main.rs には、CLI 実行時の処理が記述されているようです。

https://github.com/codemountains/sea-orm-postgresql-demo/blob/main/migration/src/main.rs

なお、ドキュメントを見ると、m20220101_000001_create_table.rsA sample migration file と記載があるので、一旦削除しました。

m20220101_000001_create_table.rs # A sample migration file

Setting Up Migration

テーブル作成

マイグレーション用のテーブル作成クエリの作成します。

今回は、ユーザーと TODO のデータを格納するテーブルを作成してみます。

sea-orm-cli migrate generate create_table_users
sea-orm-cli migrate generate create_table_todos
.
├── Cargo.lock
├── Cargo.toml
├── migration
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20230903_084439_create_table_users.rs
│       ├── m20230903_084447_create_table_todos.rs
│       └── main.rs
└── src
    └── lib.rs

SQL ではなく、コードベースでマイグレーションファイルを作成します。

https://github.com/codemountains/sea-orm-postgresql-demo/blob/main/migration/src/m20230903_084439_create_table_users.rs

TODO はユーザーとの外部キー制約を張っています。

https://github.com/codemountains/sea-orm-postgresql-demo/blob/main/migration/src/m20230903_084447_create_table_todos.rs

最後に、migrate refresh コマンドをプロジェクトのルートで実行します。

cargo run -- refresh

CLI でエンティティを作成

作成されたテーブルをもとに、エンティティを作成していきます。

sea-orm-cli generate entity \
    -u postgresql://postgres:postgres@localhost:5432/seaorm_db \
    -o src/entities
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── migration
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20230903_084439_create_table_users.rs
│       ├── m20230903_084447_create_table_todos.rs
│       └── main.rs
└── src
    ├── entities
    │   ├── mod.rs
    │   ├── prelude.rs
    │   ├── todos.rs
    │   └── users.rs
    └── lib.rs

https://github.com/codemountains/sea-orm-postgresql-demo/blob/main/src/entities/users.rs

https://github.com/codemountains/sea-orm-postgresql-demo/blob/main/src/entities/todos.rs

エンティティが自動で作成されるって、面白いアプローチですよね👀✨

  1. マイグレーションファイルを作成
  2. マイグレーションを実行
  3. CLI でエンティティファイルを作成

ちなみに、TypeORM だと以下のアプローチです。
(こっちの方が一般的になのかな?)

  1. エンティティファイルを作成
  2. CLI でマイグレーションファイルを作成
  3. マイグレーションを実行

いずれにせよ、アプリケーション主導で DB を更新する点は同じです。
メリットやデメリットに違いがあるのか、気になるところです🤔🤔

SeaORM で CRUD

コネクション

DB コネクションの生成処理です。

sqlx_logging()sqlx_logging_level() が SQLx 上に構築されていることを感じさせます🤭

pub async fn establish_connection() -> Result<DbConn, DbErr> {
    dotenv().ok();

    let url = env::var("DATABASE_URL").expect("DATABASE_URL is not found.");

    let mut opt = ConnectOptions::new(url);
    opt.max_connections(100)
        .min_connections(5)
        .connect_timeout(Duration::from_secs(8))
        .acquire_timeout(Duration::from_secs(8))
        .idle_timeout(Duration::from_secs(8))
        .max_lifetime(Duration::from_secs(8))
        .sqlx_logging(true)
        .sqlx_logging_level(log::LevelFilter::Info);

    //  DB接続のためのコネクションを生成
    Database::connect(opt).await
}

INSERT

ユーザーのインサートです。

当然、insert() でインサートなので直感的で分かりやすい。

pub async fn insert_user(db: &DbConn) -> Result<users::Model, DbErr> {
    // ユーザーアクティブモデルを生成
    let user = users::ActiveModel {
        id: ActiveValue::NotSet, // auto_increment() なのでセットしない
        name: Set("John Smith".to_string())
    };

    // insert
    let user: users::Model = user.insert(db).await?;

    Ok(user)
}

リレーションのある created_byupdated_by には、ユーザーIDをセットします。
user ではなく user.id

pub async fn insert_todos(db: &DbConn, user: &users::Model) -> Result<todos::Model, DbErr> {
    let todo = todos::ActiveModel {
        id: ActiveValue::NotSet,
        title: Set("Test".to_string()),
        description: Set("".to_string()),
        done: Default::default(),
        created_by: Set(user.id), // ユーザーIDをセット
        updated_by:  Set(user.id),
    };
    let todo: todos::Model = todo.insert(db).await?;

    Ok(todo)
}

SELECT

ID 指定で検索する関数がありました。

pub async fn select_todo(db: &DbConn, todo: todos::Model) -> Result<Option<todos::Model>, DbErr> {
    // ID 指定の検索
    let selected: Option<todos::Model> = Todos::find_by_id(todo.id).one(db).await?;
    Ok(selected)
}

Filter もあり、検索も簡単です。

pub async fn select_todos_by_user(db: &DbConn, user: &users::Model) -> Result<Vec<todos::Model>, DbErr> {
    // 作成ユーザー指定の検索
    let selected: Vec<todos::Model> = Todos::find().filter(todos::Column::CreatedBy.eq(user.id)).all(db).await?;
    Ok(selected)
}

UPDATE

アクティブモデルを生成して、値を書き換えて、update() です。

更新した情報をそのままモデルとして受け取れます。

pub async fn update_todo(db: &DbConn, todo: todos::Model) -> Result<todos::Model, DbErr> {
    // アクティブモデルを into で生成
    let mut target: todos::ActiveModel = todo.into();

    // 値を書き換える
    target.done = Set(true);

    // update
    let todo: todos::Model = target.update(db).await?;
    Ok(todo)
}

DELETE

アクティブモデルを生成して、delete() です。簡単。

pub async fn delete_todo(db: &DbConn, todo: todos::Model) -> Result<(), DbErr> {
    // アクティブモデルを into で生成
    let target: todos::ActiveModel = todo.into();

    // delete
    let _: DeleteResult = target.delete(db).await?;
    Ok(())
}

まとめ

Ruby on Rails の ActiveRecord に慣れている方には、取っ付きやすいのかな?
Rails に触れたことがないので、あまり分かりやすい感じはありませんでした。

ORM に総じて言えることですが、やはり慣れが必要ですね!

コードを見れば、どんな SQL が発行されているかは容易に想像がつくので、
学習コストはそこまで高くないのかもしれません。

Rust で ORM を採用される場合は、Diesel vs SeaORM という構図になるのでしょうか?
プロジェクトにあった選定が必要になってきそうです...

P.S. 個人的には、やはり SQLx が好きかな〜😘

参考

コラボスタイル Developers

Discussion