Rust | SQLx 上に構築された ORM "SeaORM" を試してみた
ソースコードは Github でも確認できます。
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 実行時の処理が記述されているようです。
なお、ドキュメントを見ると、m20220101_000001_create_table.rs
は A sample migration file と記載があるので、一旦削除しました。
m20220101_000001_create_table.rs # A sample migration file
テーブル作成
マイグレーション用のテーブル作成クエリの作成します。
今回は、ユーザーと 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 ではなく、コードベースでマイグレーションファイルを作成します。
TODO はユーザーとの外部キー制約を張っています。
最後に、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
エンティティが自動で作成されるって、面白いアプローチですよね👀✨
- マイグレーションファイルを作成
- マイグレーションを実行
- CLI でエンティティファイルを作成
ちなみに、TypeORM だと以下のアプローチです。
(こっちの方が一般的になのかな?)
- エンティティファイルを作成
- CLI でマイグレーションファイルを作成
- マイグレーションを実行
いずれにせよ、アプリケーション主導で 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_by
や updated_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 が好きかな〜😘
Discussion