これは、なにをしたくて書いたもの?
RustでORMというと、DieselかSeaORMが挙がるようです。
Diesel is a Safe, Extensible ORM and Query Builder for Rust
SeaORM 🐚 An async & dynamic ORM for Rust
またSQLxという線もありそうです。
今回は、まずは最近人気と言われている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
SeaORMの特徴は以下のようです。
- 非同期のサポート
- 動的なクエリーを構築可能
- モックやSQLiteを使用したテストが可能
- サービス指向で、REST、GraphQL、gRPCなどを使ったサービスを素早く構築可能
サポートしているデータベースは、MySQL、PostgreSQL、SQLiteのようです。
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つをフィーチャーとして指定することになりそうです。
以下から選択します。
ここから、SeaORMはSQLxの上に構築されたORMであることがわかりますね。
また非同期ランタイムとTLSライブラリーは組み合わせのフィーチャーとして表現されます。
今回は以下とします。
$ cargo add sea-orm --features sqlx-mysql,runtime-tokio-rustls,macros
つまりデータベースドライバーにsqlx-mysql、非同期ランタイムはtokioでTLSライブラリーは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みたいですね。
リレーションも含めて自動生成してくれるようです。このあたりの話ですね。
- One to One | SeaORM 🐚 An async & dynamic ORM for Rust
- One to Many | SeaORM 🐚 An async & dynamic ORM for Rust
- Many to Many | SeaORM 🐚 An async & dynamic ORM for Rust
では、これらを使ってみましょう。テストコードで確認していきます。
ちなみにテストの実行なのですが、テストは以下のように並列に実行しないようにしないと、データベースを同時に更新して しまうのでテストがランダムに失敗するようになります。スレッド数を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のfind
にfind_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を使っていく、でもいいかもしれませんね。