CLOVER🍀

That was when it all began.

RustのORMであるSeaORMをMySQLで試す

これは、なにをしたくて書いたもの?

RustでORMというと、DieselかSeaORMが挙がるようです。

Diesel is a Safe, Extensible ORM and Query Builder for Rust

SeaORM 🐚 An async & dynamic ORM for Rust

またSQLxという線もありそうです。

GitHub - launchbadge/sqlx: 🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.

今回は、まずは最近人気と言われているSeaORMを扱ってみようと思います。

SeaORM

SeaORMのWebサイトはこちら。

SeaORM 🐚 An async & dynamic ORM for Rust

GitHuリポジトリーはこちら。

GitHub - SeaQL/sea-orm: 🐚 An async & dynamic ORM for Rust

チュートリアル、ドキュメント、クックブック、クレートのドキュメントはこちら。

Introduction - SeaORM Tutorials

Index | SeaORM 🐚 An async & dynamic ORM for Rust

SeaORM Cookbook - SeaORM Cookbook

sea_orm - Rust

SeaORMの特徴は以下のようです。

  • 非同期のサポート
  • 動的なクエリーを構築可能
  • モックやSQLiteを使用したテストが可能
  • サービス指向で、REST、GraphQL、gRPCなどを使ったサービスを素早く構築可能

サポートしているデータベースは、MySQLPostgreSQLSQLiteのようです。

Database & Async Runtime | SeaORM 🐚 An async & dynamic ORM for Rust

コンセプトとしては、データベースをSchema、テーブルをEntity、カラムをAttribute、そしてAttributeはModelにグループ化される
といったもののようです。

SeaORM Concepts | SeaORM 🐚 An async & dynamic ORM for Rust

EntityとModelの扱いがちょっとわかりませんが、それは進めながら見ていきましょう。

他のフレームワークと統合したサンプルなどはこちら。

Tutorial & Examples | SeaORM 🐚 An async & dynamic ORM for Rust

今回はSeaORMをMySQLで試してみたいと思います。

環境

今回の環境はこちら。

$ rustup --version
rustup 1.28.1 (f9edccde0 2025-03-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.86.0 (05f9846f8 2025-03-31)`

MySQLは172.17.0.2でアクセスできるものとします。

 MySQL  localhost:33060+ ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.5     |
+-----------+
1 row in set (0.0007 sec)

準備

Cargoパッケージの作成。

$ cargo new --vcs none sea-orm-getting-started
$ cd sea-orm-getting-started

SeaORMをインストールしてHello World

ではSeaORMのクレートを追加するわけですが、まずはこちらを見た方がよさそうです。

Database & Async Runtime | SeaORM 🐚 An async & dynamic ORM for Rust

データベースドライバー、非同期ランタイム、TLSライブラリーの3つをフィーチャーとして指定することになりそうです。

以下から選択します。

  • データベースドライバー
  • 非同期ランタイム
  • TLSライブラリー
    • プラットフォームのネイティブライブラリー
    • rustls

ここから、SeaORMはSQLxの上に構築されたORMであることがわかりますね。

また非同期ランタイムとTLSライブラリーは組み合わせのフィーチャーとして表現されます。

今回は以下とします。

$ cargo add sea-orm --features sqlx-mysql,runtime-tokio-rustls,macros

つまりデータベースドライバーにsqlx-mysql、非同期ランタイムはtokioTLSライブラリーはrustlsの組み合わせということですね。

あとはtokio自体と、Entityで使うコードの都合上chronoを追加しておきます。

$ cargo add tokio --features rt-multi-thread,macros
$ cargo add chrono

Cargo.toml

[package]
name = "sea-orm-getting-started"
version = "0.1.0"
edition = "2024"

[dependencies]
chrono = "0.4.41"
sea-orm = { version = "1.1.10", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros"] }
tokio = { version = "1.44.2", features = ["rt-multi-thread", "macros"] }

ひとまず使ってみましょう。Hello World相当ということで、MySQLのバージョンを見てみます。

src/main.rs

use sea_orm::{ConnectionTrait, Database, DbErr, Statement};

#[tokio::main]
async fn  main() -> Result<(), DbErr> {
    let db = Database::connect("mysql://kazuhira:password@172.17.0.2:3306/practice").await?;

    let statement = Statement::from_string(db.get_database_backend(), "select version() as version");

    let row  = db.query_one(statement).await?.unwrap();
    let version: String = row.try_get("", "version").unwrap();
    println!("version = {}", version);

    Ok(())
}

こちらを参考にしています。

Raw SQL | SeaORM 🐚 An async & dynamic ORM for Rust

実行。

$ cargo run

動作しましたね。

version = 8.4.5

SeaORMでEntityを使う

続いて、Entityを使ってみます。チュートリアルでは自動生成するようになっているので、こちらもそうしてみましょう。

Using sea-orm-cli | SeaORM 🐚 An async & dynamic ORM for Rust

Generate Entity from Database - SeaORM Tutorials

なお、マイグレーションについては今回は飛ばしました。

DDLはこんな感じで用意。

create table book(
  isbn varchar(14),
  title varchar(255) not null,
  publish_date date not null,
  price integer not null,
  primary key(isbn)
);

create table user(
  id integer auto_increment,
  first_name varchar(10) not null,
  last_name varchar(10) not null,
  age integer not null,
  primary key(id)
);

create table post(
  id integer auto_increment ,
  title varchar(255) not null,
  url varchar(255) not null,
  user_id integer not null,
  primary key(id),
  foreign key(user_id) references user(id)
);

Entityを生成したり、マイグレーションを実行するにはsea-orm-cliを使います。

デフォルトだとOpenSSLを使って構築しようとするるみたいなので、rustlsに切り替えました。なお、非同期ランタイムも
フィーチャーでtokioに切り替えられるのですが、コード自体はasync-stdで固定されているみたいなのでこちらに合わせました…。

$ cargo install --root tools sea-orm-cli --no-default-features --features codegen,cli,runtime-async-std-rustls,async-std

バージョン。

$ tools/bin/sea-orm-cli --version
sea-orm-cli 1.1.10

以下のコマンドでEntityを自動生成します。

$ tools/bin/sea-orm-cli generate entity \
  -u mysql://kazuhira:password@172.17.0.2:3306/practice \
  -o src/entities

実行時のログ。

Connecting to MySQL ...
Discovering schema ...
... discovered.
Generating book.rs
    > Column `isbn`: String, not_null
    > Column `title`: String, not_null
    > Column `publish_date`: Date, not_null
    > Column `price`: i32, not_null
Generating post.rs
    > Column `id`: i32, auto_increment, not_null
    > Column `title`: String, not_null
    > Column `url`: String, not_null
    > Column `user_id`: i32, not_null
Generating user.rs
    > Column `id`: i32, auto_increment, not_null
    > Column `first_name`: String, not_null
    > Column `last_name`: String, not_null
    > Column `age`: i32, not_null
Writing src/entities/book.rs
Writing src/entities/post.rs
Writing src/entities/user.rs
Writing src/entities/mod.rs
Writing src/entities/prelude.rs
... Done.

生成されたコードを見てみます。

src/entities/mod.rs

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

pub mod prelude;

pub mod book;
pub mod post;
pub mod user;

src/entities/prelude.rs

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

pub use super::book::Entity as Book;
pub use super::post::Entity as Post;
pub use super::user::Entity as User;

src/entities/book.rs

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "book")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub isbn: String,
    pub title: String,
    pub publish_date: Date,
    pub price: i32,
}

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

impl ActiveModelBehavior for ActiveModel {}

src/entities/user.rs

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub first_name: String,
    pub last_name: String,
    pub age: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(has_many = "super::post::Entity")]
    Post,
}

impl Related<super::post::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Post.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

src/entities/post.rs

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub title: String,
    pub url: String,
    pub user_id: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::user::Entity",
        from = "Column::UserId",
        to = "super::user::Column::Id",
        on_update = "NoAction",
        on_delete = "NoAction"
    )]
    User,
}

impl Related<super::user::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

こう見ると、モジュールがEntityで実際のテーブル定義を表したものはModelみたいですね。

リレーションも含めて自動生成してくれるようです。このあたりの話ですね。

では、これらを使ってみましょう。テストコードで確認していきます。

ちなみにテストの実行なのですが、テストは以下のように並列に実行しないようにしないと、データベースを同時に更新して   しまうのでテストがランダムに失敗するようになります。スレッド数を1にしておきます。

$ cargo test -- --test-threads=1

先ほどのmain.tsを以下のように変更。

src/main.rs

use sea_orm::{ConnectionTrait, Database, DbErr, Statement};

#[tokio::main]
async fn main() -> Result<(), DbErr> {
    let db = Database::connect("mysql://kazuhira:password@172.17.0.2:3306/practice").await?;

    let statement =
        Statement::from_string(db.get_database_backend(), "select version() as version");

    let row = db.query_one(statement).await?.unwrap();
    let version: String = row.try_get("", "version").unwrap();
    println!("version = {}", version);

    Ok(())
}

mod entities;

#[cfg(test)]
mod tests {
    use chrono::NaiveDate;
    use sea_orm::{
        ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, Database, DatabaseConnection,
        DbErr, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, Statement,
        TransactionTrait,
    };

    use crate::entities::book::Entity as Book;
    use crate::entities::post::{self, Entity as Post};
    use crate::entities::user::Entity as User;
    use crate::entities::{book, user};

    async fn connect_db() -> Result<DatabaseConnection, DbErr> {
        Database::connect("mysql://kazuhira:password@172.17.0.2:3306/practice").await
    }

    async fn before_cleanup(db: &DatabaseConnection) {
        let transaction = db.begin().await.unwrap();

        transaction
            .execute(Statement::from_string(
                transaction.get_database_backend(),
                "truncate table book",
            ))
            .await
            .unwrap();

        Post::delete_many().exec(&transaction).await.unwrap();
        User::delete_many().exec(&transaction).await.unwrap();

        transaction.commit().await.unwrap();
    }

    // ここにテストを書く!
}

生成したEntityモジュールを宣言。

mod entities;

ドキュメントも自動生成したEntityもそうでしたが、[Entity名]::Entityに別名を付けた方が扱いやすいようです。

    use crate::entities::book::Entity as Book;
    use crate::entities::post::{self, Entity as Post};
    use crate::entities::user::Entity as User;
    use crate::entities::{book, user};

あとはテスト用のデータベース接続と、テスト内でデータを削除するための関数を作成しています。

    async fn connect_db() -> Result<DatabaseConnection, DbErr> {
        Database::connect("mysql://kazuhira:password@172.17.0.2:3306/practice").await
    }

    async fn before_cleanup(db: &DatabaseConnection) {
        let transaction = db.begin().await.unwrap();

        transaction
            .execute(Statement::from_string(
                transaction.get_database_backend(),
                "truncate table book",
            ))
            .await
            .unwrap();

        Post::delete_many().exec(&transaction).await.unwrap();
        User::delete_many().exec(&transaction).await.unwrap();

        transaction.commit().await.unwrap();
    }

まだ出てきていない要素も使っていますけど。

基本的なCRUDから。

    #[tokio::test]
    async fn basic_crud() {
        let db = connect_db().await.unwrap();

        before_cleanup(&db).await;

        let rust_book = book::ActiveModel {
            isbn: ActiveValue::Set("978-4873119786".to_string()),
            title: ActiveValue::Set("プログラミングRust 第2版".to_string()),
            publish_date: ActiveValue::Set(NaiveDate::from_ymd_opt(2022, 1, 19).unwrap()),
            price: ActiveValue::Set(5280),
        };

        // insert
        rust_book.insert(&db).await.unwrap();

        let rust_web_application = book::ActiveModel {
            isbn: ActiveValue::Set("978-4065369579".to_string()),
            title: ActiveValue::Set(
                "RustによるWebアプリケーション開発 設計からリリース・運用まで".to_string(),
            ),
            publish_date: ActiveValue::Set(NaiveDate::from_ymd_opt(2024, 9, 30).unwrap()),
            price: ActiveValue::Set(4400),
        };

        // insert
        rust_web_application.insert(&db).await.unwrap();

        // select one
        let result_rust_book = Book::find_by_id("978-4873119786")
            .one(&db)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(result_rust_book.isbn, "978-4873119786");
        assert_eq!(result_rust_book.title, "プログラミングRust 第2版");
        assert_eq!(
            result_rust_book.publish_date,
            NaiveDate::from_ymd_opt(2022, 1, 19).unwrap()
        );
        assert_eq!(result_rust_book.price, 5280);

        // select many
        let result_books = Book::find()
            .order_by_asc(book::Column::Price)
            .all(&db)
            .await
            .unwrap();

        assert_eq!(result_books.len(), 2);
        assert_eq!(
            result_books[0].title,
            "RustによるWebアプリケーション開発 設計からリリース・運用まで"
        );
        assert_eq!(result_books[1].title, "プログラミングRust 第2版");

        // count
        let before_count = Book::find().count(&db).await.unwrap();
        assert_eq!(before_count, 2);

        // delete
        let delete_affected = Book::delete_by_id("978-4065369579")
            .exec(&db)
            .await
            .unwrap();
        assert_eq!(delete_affected.rows_affected, 1);

        let after_count = Book::find().count(&db).await.unwrap();
        assert_eq!(after_count, 1);

        let mut update_rust_book: book::ActiveModel = Book::find_by_id("978-4873119786")
            .one(&db)
            .await
            .unwrap()
            .unwrap()
            .into();
        update_rust_book.price = ActiveValue::Set(10000);
        // update
        update_rust_book.update(&db).await.unwrap();

        assert_eq!(
            Book::find_by_id("978-4873119786")
                .one(&db)
                .await
                .unwrap()
                .unwrap()
                .price,
            10000
        );
    }

Basic CRUD Operations - SeaORM Tutorials

insert。データはActiveModelという構造体を使って作成するようです。insert自体も構造体のインスタンスに対して実行します。

        let rust_book = book::ActiveModel {
            isbn: ActiveValue::Set("978-4873119786".to_string()),
            title: ActiveValue::Set("プログラミングRust 第2版".to_string()),
            publish_date: ActiveValue::Set(NaiveDate::from_ymd_opt(2022, 1, 19).unwrap()),
            price: ActiveValue::Set(5280),
        };

        // insert
        rust_book.insert(&db).await.unwrap();

Insert | SeaORM 🐚 An async & dynamic ORM for Rust

selectで1件取得。oneを使っているので1件です。戻り値はResult<Option<E::Model>, DbError>になります。

        // select one
        let result_rust_book = Book::find_by_id("978-4873119786")
            .one(&db)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(result_rust_book.isbn, "978-4873119786");
        assert_eq!(result_rust_book.title, "プログラミングRust 第2版");
        assert_eq!(
            result_rust_book.publish_date,
            NaiveDate::from_ymd_opt(2022, 1, 19).unwrap()
        );
        assert_eq!(result_rust_book.price, 5280);

Select | SeaORM 🐚 An async & dynamic ORM for Rust

この場合の起点は、別名を付けたbook::Entityですね。

Book::find_by_id("978-4873119786")

allで複数件取得。戻り値はResult<Vec<Model>>になります。

        // select many
        let result_books = Book::find()
            .order_by_asc(book::Column::Price)
            .all(&db)
            .await
            .unwrap();

        assert_eq!(result_books.len(), 2);
        assert_eq!(
            result_books[0].title,
            "RustによるWebアプリケーション開発 設計からリリース・運用まで"
        );
        assert_eq!(result_books[1].title, "プログラミングRust 第2版");

count。

        // count
        let before_count = Book::find().count(&db).await.unwrap();
        assert_eq!(before_count, 2);

ColumnTrait in sea_orm::entity - Rust

delete。後半は結果の確認ですね。

        // delete
        let delete_affected = Book::delete_by_id("978-4065369579")
            .exec(&db)
            .await
            .unwrap();
        assert_eq!(delete_affected.rows_affected, 1);

        let after_count = Book::find().count(&db).await.unwrap();
        assert_eq!(after_count, 1);

Delete | SeaORM 🐚 An async & dynamic ORM for Rust

update。対象が1件の場合は、最初にModelを取得してModelに対してupdateを実行します。

        let mut update_rust_book: book::ActiveModel = Book::find_by_id("978-4873119786")
            .one(&db)
            .await
            .unwrap()
            .unwrap()
            .into();
        update_rust_book.price = ActiveValue::Set(10000);
        // update
        update_rust_book.update(&db).await.unwrap();

        assert_eq!(
            Book::find_by_id("978-4873119786")
                .one(&db)
                .await
                .unwrap()
                .unwrap()
                .price,
            10000
        );

Update | SeaORM 🐚 An async & dynamic ORM for Rust

ちなみに、プライマリーキーがNotSetなのかSetなのかでinsertかupdateに振り分けるsaveというものもあるようです。

Save | SeaORM 🐚 An async & dynamic ORM for Rust

次はトランザクションを使ってみます。

    #[tokio::test]
    async fn with_transaction() {
        let db = connect_db().await.unwrap();

        before_cleanup(&db).await;

        let rust_book = book::ActiveModel {
            isbn: ActiveValue::Set("978-4873119786".to_string()),
            title: ActiveValue::Set("プログラミングRust 第2版".to_string()),
            publish_date: ActiveValue::Set(NaiveDate::from_ymd_opt(2022, 1, 19).unwrap()),
            price: ActiveValue::Set(5280),
        };

        let rust_web_application = book::ActiveModel {
            isbn: ActiveValue::Set("978-4065369579".to_string()),
            title: ActiveValue::Set(
                "RustによるWebアプリケーション開発 設計からリリース・運用まで".to_string(),
            ),
            publish_date: ActiveValue::Set(NaiveDate::from_ymd_opt(2024, 9, 30).unwrap()),
            price: ActiveValue::Set(4400),
        };

        let transaction = db.begin().await.unwrap();

        let _ = rust_book.clone().insert(&transaction).await;
        let _ = rust_web_application.clone().insert(&transaction).await;

        transaction.rollback().await.unwrap();

        assert_eq!(Book::find().count(&db).await.unwrap(), 0);

        let transaction = db.begin().await.unwrap();

        let _ = rust_book.clone().insert(&transaction).await;
        let _ = rust_web_application.clone().insert(&transaction).await;

        transaction.commit().await.unwrap();

        assert_eq!(Book::find().count(&db).await.unwrap(), 2);
    }

Transaction | SeaORM 🐚 An async & dynamic ORM for Rust

先ほどとの違いは、DatabaseConnectionを使っていたところをDatabaseTransactionに置き換えるところですね。

        let _ = rust_book.clone().insert(&transaction).await;
        let _ = rust_web_application.clone().insert(&transaction).await;

トランザクション自体は、DatabaseConnection#beginで取得します。

        let transaction = db.begin().await.unwrap();

そしてコミットまたはロールバックします。

        transaction.commit().await.unwrap();


        transaction.rollback().await.unwrap();

ちなみに、明示的にロールバックしなくてもトランザクションはスコープを外れると自動的にロールバックするようです。

begin the transaction followed by a commit or rollback. If txn goes out of scope, the transaction is automatically rollbacked.

Transaction | SeaORM 🐚 An async & dynamic ORM for Rust

リレーション。

    #[tokio::test]
    async fn relation() {
        let db = connect_db().await.unwrap();

        before_cleanup(&db).await;

        let transaction = db.begin().await.unwrap();

        let katsuo = user::ActiveModel {
            first_name: ActiveValue::Set("カツオ".to_string()),
            last_name: ActiveValue::Set("磯野".to_string()),
            age: ActiveValue::Set(11),
            ..Default::default()
        };

        let katsuo_id = User::insert(katsuo)
            .exec(&transaction)
            .await
            .unwrap()
            .last_insert_id;

        let katsuo_posts = vec![
            post::ActiveModel {
                title: ActiveValue::Set(
                    "Ubuntu Linux 24.04 LTSでRustを始める(+Emacs lsp-mode)".to_string(),
                ),
                url: ActiveValue::Set(
                    "https://kazuhira-r.hatenablog.com/entry/2025/01/01/115017".to_string(),
                ),
                user_id: ActiveValue::Set(katsuo_id),
                ..Default::default()
            },
            post::ActiveModel {
                title: ActiveValue::Set("RustのフォーマッターRustfmtとリンターClippy".to_string()),
                url: ActiveValue::Set(
                    "https://kazuhira-r.hatenablog.com/entry/2025/01/01/115055".to_string(),
                ),
                user_id: ActiveValue::Set(katsuo_id),
                ..Default::default()
            },
        ];

        for post in katsuo_posts {
            let _ = Post::insert(post).exec(&transaction).await;
        }

        let wakame = user::ActiveModel {
            first_name: ActiveValue::Set("ワカメ".to_string()),
            last_name: ActiveValue::Set("磯野".to_string()),
            age: ActiveValue::Set(9),
            ..Default::default()
        };

        let wakame_id = User::insert(wakame)
            .exec(&transaction)
            .await
            .unwrap()
            .last_insert_id;

        let wakame_posts = vec![post::ActiveModel {
            title: ActiveValue::Set(
                "RustのGetting Started「Command line apps in Rust」を試す".to_string(),
            ),
            url: ActiveValue::Set(
                "https://kazuhira-r.hatenablog.com/entry/2025/01/01/182902".to_string(),
            ),
            user_id: ActiveValue::Set(wakame_id),
            ..Default::default()
        }];

        for post in wakame_posts {
            let _ = Post::insert(post).exec(&transaction).await;
        }

        assert_eq!(Post::find().count(&transaction).await.unwrap(), 3);

        // One to Many(Eager)
        let katsuo_and_posts = User::find()
            .filter(user::Column::FirstName.eq("カツオ"))
            .find_also_related(Post)
            .order_by_asc(post::Column::Id)
            .all(&transaction)
            .await
            .unwrap();

        let (k, p) = &katsuo_and_posts[0];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "Ubuntu Linux 24.04 LTSでRustを始める(+Emacs lsp-mode)"
        );

        let (k, p) = &katsuo_and_posts[1];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "RustのフォーマッターRustfmtとリンターClippy"
        );

        // One to One(Lazy)
        let wakame_post = Post::find()
            .filter(
                post::Column::Url.eq("https://kazuhira-r.hatenablog.com/entry/2025/01/01/182902"),
            )
            .one(&transaction)
            .await
            .unwrap();
        let post: post::Model = wakame_post.unwrap();
        let wakame = post.find_related(User).one(&transaction).await.unwrap();

        assert_eq!(
            post.title,
            "RustのGetting Started「Command line apps in Rust」を試す"
        );
        assert_eq!(wakame.unwrap().first_name, "ワカメ");

        transaction.commit().await.unwrap();
    }

Select | SeaORM 🐚 An async & dynamic ORM for Rust

Relational Select - SeaORM Tutorials

データ登録。

        let katsuo = user::ActiveModel {
            first_name: ActiveValue::Set("カツオ".to_string()),
            last_name: ActiveValue::Set("磯野".to_string()),
            age: ActiveValue::Set(11),
            ..Default::default()
        };

        let katsuo_id = User::insert(katsuo)
            .exec(&transaction)
            .await
            .unwrap()
            .last_insert_id;

        let katsuo_posts = vec![
            post::ActiveModel {
                title: ActiveValue::Set(
                    "Ubuntu Linux 24.04 LTSでRustを始める(+Emacs lsp-mode)".to_string(),
                ),
                url: ActiveValue::Set(
                    "https://kazuhira-r.hatenablog.com/entry/2025/01/01/115017".to_string(),
                ),
                user_id: ActiveValue::Set(katsuo_id),
                ..Default::default()
            },
            post::ActiveModel {
                title: ActiveValue::Set("RustのフォーマッターRustfmtとリンターClippy".to_string()),
                url: ActiveValue::Set(
                    "https://kazuhira-r.hatenablog.com/entry/2025/01/01/115055".to_string(),
                ),
                user_id: ActiveValue::Set(katsuo_id),
                ..Default::default()
            },
        ];

        for post in katsuo_posts {
            let _ = Post::insert(post).exec(&transaction).await;
        }

        let wakame = user::ActiveModel {
            first_name: ActiveValue::Set("ワカメ".to_string()),
            last_name: ActiveValue::Set("磯野".to_string()),
            age: ActiveValue::Set(9),
            ..Default::default()
        };

        let wakame_id = User::insert(wakame)
            .exec(&transaction)
            .await
            .unwrap()
            .last_insert_id;

        let wakame_posts = vec![post::ActiveModel {
            title: ActiveValue::Set(
                "RustのGetting Started「Command line apps in Rust」を試す".to_string(),
            ),
            url: ActiveValue::Set(
                "https://kazuhira-r.hatenablog.com/entry/2025/01/01/182902".to_string(),
            ),
            user_id: ActiveValue::Set(wakame_id),
            ..Default::default()
        }];

        for post in wakame_posts {
            let _ = Post::insert(post).exec(&transaction).await;
        }

        assert_eq!(Post::find().count(&transaction).await.unwrap(), 3);

今回のテーブルはプライマリーキーをauto incrementにしていますが、その場合は..Default::default()と書いておけばよさそうです。

        let katsuo = user::ActiveModel {
            first_name: ActiveValue::Set("カツオ".to_string()),
            last_name: ActiveValue::Set("磯野".to_string()),
            age: ActiveValue::Set(11),
            ..Default::default()
        };

またauto incrementでinsertを実行した時に生成された値を取得するには以下のようになります。

        let katsuo_id = User::insert(katsuo)
            .exec(&transaction)
            .await
            .unwrap()
            .last_insert_id;

Modelからのinsertではなくなりますね。

One to Many。

        // One to Many(Eager)
        let katsuo_and_posts = User::find()
            .filter(user::Column::FirstName.eq("カツオ"))
            .find_also_related(Post)
            .order_by_asc(post::Column::Id)
            .all(&transaction)
            .await
            .unwrap();

        let (k, p) = &katsuo_and_posts[0];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "Ubuntu Linux 24.04 LTSでRustを始める(+Emacs lsp-mode)"
        );

        let (k, p) = &katsuo_and_posts[1];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "RustのフォーマッターRustfmtとリンターClippy"
        );

Entityのfindfind_also_relatedをつなげ、Eager Loadingと呼ばれるものにしています。

        // One to Many(Eager)
        let katsuo_and_posts = User::find()
            .filter(user::Column::FirstName.eq("カツオ"))
            .find_also_related(Post)
            .order_by_asc(post::Column::Id)
            .all(&transaction)
            .await
            .unwrap();

Select | SeaORM 🐚 An async & dynamic ORM for Rust

find_also_relatedした結果はちょっと変わっていて(E::Model, Option<F::Model>)のタプルになります。今回はall
使っているので、Result<Vec<(E::Model, Option<F::Model>)>>になります。

よって、今回の例だとカツオを表すUserインスタンスが2回登場します。

        let (k, p) = &katsuo_and_posts[0];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "Ubuntu Linux 24.04 LTSでRustを始める(+Emacs lsp-mode)"
        );

        let (k, p) = &katsuo_and_posts[1];
        assert_eq!(k.first_name, "カツオ");
        assert_eq!(
            p.as_ref().unwrap().title,
            "RustのフォーマッターRustfmtとリンターClippy"
        );

他の言語でよく見るORMと違い、join元のModelのプロパティから取得するような形式ではないですね。

One to One。

        // One to One(Lazy)
        let wakame_post = Post::find()
            .filter(
                post::Column::Url.eq("https://kazuhira-r.hatenablog.com/entry/2025/01/01/182902"),
            )
            .one(&transaction)
            .await
            .unwrap();
        let post: post::Model = wakame_post.unwrap();
        let wakame = post.find_related(User).one(&transaction).await.unwrap();

        assert_eq!(
            post.title,
            "RustのGetting Started「Command line apps in Rust」を試す"
        );
        assert_eq!(wakame.unwrap().first_name, "ワカメ");

こちらはLazy Loadingとしています。find_relatedを後で実行する感じですね。

        let wakame = post.find_related(User).one(&transaction).await.unwrap();

実行されるSQLの違いも見ておきます。

Eager Loading。こちらはjoinして取得しています。

2025-05-03T16:07:41.945157Z        11 Prepare   SELECT `user`.`id` AS `A_id`, `user`.`first_name` AS `A_first_name`, `user`.`last_name` AS `A_last_name`, `user`.`age` AS `A_age`, `post`.`id` AS `B_id`, `post`.`title` AS `B_title`, `post`.`url` AS `B_url`, `post`.`user_id` AS `B_user_id` FROM `user` LEFT JOIN `post` ON `user`.`id` = `post`.`user_id` WHERE `user`.`first_name` = ? ORDER BY `post`.`id` ASC
2025-05-03T16:07:41.945697Z        11 Execute   SELECT `user`.`id` AS `A_id`, `user`.`first_name` AS `A_first_name`, `user`.`last_name` AS `A_last_name`, `user`.`age` AS `A_age`, `post`.`id` AS `B_id`, `post`.`title` AS `B_title`, `post`.`url` AS `B_url`, `post`.`user_id` AS `B_user_id` FROM `user` LEFT JOIN `post` ON `user`.`id` = `post`.`user_id` WHERE `user`.`first_name` = 'カツオ' ORDER BY `post`.`id` ASC

Lazy Loading。当たり前ですが、こちらはSQLが別々になりますね。

2025-05-03T16:07:41.947376Z        11 Prepare   SELECT `post`.`id`, `post`.`title`, `post`.`url`, `post`.`user_id` FROM `post` WHERE `post`.`url` = ? LIMIT ?
2025-05-03T16:07:41.947731Z        11 Execute   SELECT `post`.`id`, `post`.`title`, `post`.`url`, `post`.`user_id` FROM `post` WHERE `post`.`url` = 'https://kazuhira-r.hatenablog.com/entry/2025/01/01/182902' LIMIT 1
2025-05-03T16:07:41.948731Z        11 Prepare   SELECT `user`.`id`, `user`.`first_name`, `user`.`last_name`, `user`.`age` FROM `user` INNER JOIN `post` ON `post`.`user_id` = `user`.`id` WHERE `post`.`id` = ? LIMIT ?
2025-05-03T16:07:41.949048Z        11 Execute   SELECT `user`.`id`, `user`.`first_name`, `user`.`last_name`, `user`.`age` FROM `user` INNER JOIN `post` ON `post`.`user_id` = `user`.`id` WHERE `post`.`id` = 44 LIMIT 1

こんなところでしょうか。

おわりに

RustのORMであるSeaORMを試してみました。

最初は依存関係のインストールに手こずったり、どこから初めていいのかよくわからなかったりしましたが、慣れるとなんとか
進められるようになりました。

最終的にチュートリアルよりもドキュメントを見ていることの方が多かった気もしますね。

慣れると便利そうなので、SeaORMを使っていく、でもいいかもしれませんね。