SeaORM
SeaORMは「Rustのための非同期で動的なORM」として開発された、現代的なRustエコシステムで注目されているORM(Object-Relational Mapping)ライブラリです。非同期処理をネイティブサポートし、PostgreSQL、MySQL、SQLite対応の強力な型安全性を提供。SeaQLチームによって開発され、関数型リレーショナルマッピング(FRM)アプローチにより、コンパイル時の型チェックと優れたパフォーマンスを実現しています。Axum、Actix-web等の主要Webフレームワークとの統合も容易で、スキーマ駆動開発とコード生成によりRustの生産性を大幅に向上させます。
GitHub概要
SeaQL/sea-orm
🐚 An async & dynamic ORM for Rust
トピックス
スター履歴
ライブラリ
SeaORM
概要
SeaORMは「Rustのための非同期で動的なORM」として開発された、現代的なRustエコシステムで注目されているORM(Object-Relational Mapping)ライブラリです。非同期処理をネイティブサポートし、PostgreSQL、MySQL、SQLite対応の強力な型安全性を提供。SeaQLチームによって開発され、関数型リレーショナルマッピング(FRM)アプローチにより、コンパイル時の型チェックと優れたパフォーマンスを実現しています。Axum、Actix-web等の主要Webフレームワークとの統合も容易で、スキーマ駆動開発とコード生成によりRustの生産性を大幅に向上させます。
詳細
SeaORM 2025年版は、Rustの非同期プログラミングとデータベースアクセスの最良の組み合わせを提供する成熟したORMライブラリです。async/await
構文のフルサポートにより、Tokio、async-std等の非同期ランタイムと完全に統合され、高いスループットを実現。エンティティ定義にはDeriveEntityModel
マクロを使用し、データベーススキーマからRust構造体への自動変換をサポート。マイグレーション機能ではsea-orm-migration
により、データベーススキーマの進化を安全かつ段階的に管理可能です。
主な特徴
- 完全非同期対応: Tokio、async-stdでの高性能非同期処理
- 強力な型安全性: コンパイル時エラー検出と自動補完
- マルチデータベース対応: PostgreSQL、MySQL、SQLiteをサポート
- スキーマ駆動開発: データベーススキーマからエンティティコード生成
- 高度なクエリ構築: 型安全なクエリビルダーとリレーション操作
- マイグレーション管理: バージョン管理されたスキーマ変更
メリット・デメリット
メリット
- Rustの所有権システムとの完全な統合による高い安全性とパフォーマンス
- 非同期処理のネイティブサポートによる高いスループット実現
- 強力な型システムによるコンパイル時エラー検出とバグ減少
- マルチデータベース対応により、プロジェクト要件に応じた柔軟な選択
- 充実したドキュメントと活発なコミュニティサポート
- CLI ツールによるコード生成とマイグレーション管理の自動化
デメリット
- Rustの学習コストが高く、初学者には敷居が高い
- 複雑なクエリではRaw SQLへのフォールバックが必要
- Node.js等の他言語エコシステムと比較してライブラリ選択肢が限定的
- デバッグ時のエラーメッセージが複雑になる場合がある
- 新しいライブラリのため、レガシーシステムとの統合事例が少ない
- コンパイル時間が長くなる場合がある(特に大規模プロジェクト)
参考ページ
書き方の例
セットアップとプロジェクト初期化
# Cargo.tomlに依存関係を追加
cargo add sea-orm
cargo add sea-orm-cli --dev
# PostgreSQL機能を有効化
cargo add sea-orm --features "sqlx-postgres,runtime-tokio-rustls,macros"
# マイグレーション用
cargo add sea-orm-migration
# CLIツールのインストール
cargo install sea-orm-cli
基本的なエンティティ定義
use sea_orm::entity::prelude::*;
// User エンティティの定義
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub email: String,
pub created_at: DateTimeUtc,
pub updated_at: Option<DateTimeUtc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::post::Entity")]
Posts,
}
impl Related<super::post::Entity> for Entity {
fn to() -> RelationDef {
Relation::Posts.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
// Post エンティティの定義
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "posts")]
pub struct PostModel {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub title: String,
pub content: String,
pub published: bool,
pub created_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum PostRelation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for super::post::Entity {
fn to() -> RelationDef {
PostRelation::User.def()
}
}
データベース接続とクエリ操作
use sea_orm::*;
// データベース接続の確立
#[tokio::main]
async fn main() -> Result<(), DbErr> {
// PostgreSQL接続
let db = Database::connect("postgresql://user:password@localhost/database").await?;
// 接続オプションの設定
let mut opt = ConnectOptions::new("postgresql://user:password@localhost/database");
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8));
let db = Database::connect(opt).await?;
// 全ユーザーの取得
let users: Vec<user::Model> = User::find().all(&db).await?;
println!("Found {} users", users.len());
// 条件フィルタリング
let active_users: Vec<user::Model> = User::find()
.filter(user::Column::Email.contains("@example.com"))
.all(&db)
.await?;
// 単一レコードの取得
let user: Option<user::Model> = User::find_by_id(1).one(&db).await?;
if let Some(user) = user {
println!("User: {}", user.name);
}
// ページネーション
let paginator = User::find()
.order_by_asc(user::Column::Id)
.paginate(&db, 10);
let users_page_1 = paginator.fetch_page(0).await?;
let total_pages = paginator.num_pages().await?;
Ok(())
}
レコードの作成と更新
use sea_orm::*;
use chrono::Utc;
async fn create_and_update_users(db: &DatabaseConnection) -> Result<(), DbErr> {
// 新しいユーザーの作成
let new_user = user::ActiveModel {
name: Set("田中太郎".to_owned()),
email: Set("[email protected]".to_owned()),
created_at: Set(Utc::now()),
..Default::default()
};
let user: user::Model = new_user.insert(db).await?;
println!("Created user with ID: {}", user.id);
// 複数レコードの一括挿入
let users_data = vec![
user::ActiveModel {
name: Set("佐藤花子".to_owned()),
email: Set("[email protected]".to_owned()),
created_at: Set(Utc::now()),
..Default::default()
},
user::ActiveModel {
name: Set("鈴木次郎".to_owned()),
email: Set("[email protected]".to_owned()),
created_at: Set(Utc::now()),
..Default::default()
},
];
let insert_result = User::insert_many(users_data).exec(db).await?;
println!("Inserted {} users", insert_result.rows_affected);
// レコードの更新
let user_to_update: user::ActiveModel = User::find_by_id(1)
.one(db)
.await?
.unwrap()
.into();
let mut user_to_update = user_to_update;
user_to_update.name = Set("田中太郎(更新済み)".to_owned());
user_to_update.updated_at = Set(Some(Utc::now()));
let updated_user: user::Model = user_to_update.update(db).await?;
println!("Updated user: {}", updated_user.name);
// 条件一括更新
User::update_many()
.col_expr(user::Column::UpdatedAt, Expr::value(Utc::now()))
.filter(user::Column::CreatedAt.lt(Utc::now() - Duration::days(30)))
.exec(db)
.await?;
Ok(())
}
リレーション操作とJoinクエリ
use sea_orm::*;
async fn work_with_relations(db: &DatabaseConnection) -> Result<(), DbErr> {
// 遅延読み込み(Lazy Loading)
let user = User::find_by_id(1).one(db).await?.unwrap();
let posts: Vec<post::Model> = user.find_related(Post).all(db).await?;
println!("User {} has {} posts", user.name, posts.len());
// 積極読み込み(Eager Loading)
let users_with_posts: Vec<(user::Model, Vec<post::Model>)> = User::find()
.find_with_related(Post)
.all(db)
.await?;
for (user, posts) in users_with_posts {
println!("User: {}, Posts: {}", user.name, posts.len());
}
// カスタムJoin
let users_with_published_posts = User::find()
.join(JoinType::InnerJoin, user::Relation::Posts.def())
.filter(post::Column::Published.eq(true))
.group_by(user::Column::Id)
.all(db)
.await?;
// 複雑なクエリとサブクエリ
let active_users = User::find()
.filter(
user::Column::Id.in_subquery(
Query::select()
.column(post::Column::UserId)
.from(post::Entity)
.and_where(post::Column::CreatedAt.gte(Utc::now() - Duration::days(7)))
.to_owned()
)
)
.all(db)
.await?;
// 集約関数の使用
let post_count: Option<i64> = Post::find()
.filter(post::Column::Published.eq(true))
.count(db)
.await
.ok();
println!("Published posts count: {:?}", post_count);
Ok(())
}
マイグレーション管理
use sea_orm_migration::prelude::*;
// マイグレーション定義
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(
ColumnDef::new(User::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(User::Name).string().not_null())
.col(
ColumnDef::new(User::Email)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(User::CreatedAt)
.timestamp()
.not_null(),
)
.col(ColumnDef::new(User::UpdatedAt).timestamp())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
#[derive(Iden)]
enum User {
Table,
Id,
Name,
Email,
CreatedAt,
UpdatedAt,
}
// マイグレーション実行
#[tokio::main]
async fn main() -> Result<(), DbErr> {
let db = Database::connect("postgresql://user:password@localhost/database").await?;
let schema_manager = SchemaManager::new(&db);
Migration.up(&schema_manager).await?;
println!("Migration applied successfully!");
Ok(())
}
エラーハンドリングとトランザクション
use sea_orm::*;
async fn transaction_example(db: &DatabaseConnection) -> Result<(), DbErr> {
// トランザクション処理
let txn = db.begin().await?;
let user_result = user::ActiveModel {
name: Set("トランザクションユーザー".to_owned()),
email: Set("[email protected]".to_owned()),
created_at: Set(Utc::now()),
..Default::default()
}.insert(&txn).await;
match user_result {
Ok(user) => {
let post_result = post::ActiveModel {
user_id: Set(user.id),
title: Set("初投稿".to_owned()),
content: Set("トランザクション内で作成された投稿".to_owned()),
published: Set(true),
created_at: Set(Utc::now()),
..Default::default()
}.insert(&txn).await;
match post_result {
Ok(_) => {
txn.commit().await?;
println!("Transaction committed successfully");
}
Err(e) => {
txn.rollback().await?;
println!("Post creation failed, transaction rolled back: {}", e);
}
}
}
Err(e) => {
txn.rollback().await?;
println!("User creation failed, transaction rolled back: {}", e);
}
}
// 関数型トランザクション
db.transaction::<_, (), DbErr>(|txn| {
Box::pin(async move {
let user = user::ActiveModel {
name: Set("関数型ユーザー".to_owned()),
email: Set("[email protected]".to_owned()),
created_at: Set(Utc::now()),
..Default::default()
}.insert(txn).await?;
post::ActiveModel {
user_id: Set(user.id),
title: Set("関数型投稿".to_owned()),
content: Set("関数型トランザクション内で作成".to_owned()),
published: Set(true),
created_at: Set(Utc::now()),
..Default::default()
}.insert(txn).await?;
Ok(())
})
}).await?;
// エラーハンドリングパターン
match User::find_by_id(999).one(db).await {
Ok(Some(user)) => println!("Found user: {}", user.name),
Ok(None) => println!("User not found"),
Err(DbErr::ConnectionAcquire(e)) => {
eprintln!("Database connection error: {}", e);
}
Err(DbErr::Query(RuntimeErr::SqlxError(e))) => {
eprintln!("SQL execution error: {}", e);
}
Err(e) => {
eprintln!("Other database error: {}", e);
}
}
Ok(())
}
パフォーマンス最適化
use sea_orm::*;
async fn performance_optimization(db: &DatabaseConnection) -> Result<(), DbErr> {
// バッチ操作
let batch_size = 1000;
let mut users_batch = Vec::with_capacity(batch_size);
for i in 0..batch_size {
users_batch.push(user::ActiveModel {
name: Set(format!("Batch User {}", i)),
email: Set(format!("batch{}@example.com", i)),
created_at: Set(Utc::now()),
..Default::default()
});
}
User::insert_many(users_batch).exec(db).await?;
// 部分フィールド選択
let user_names: Vec<String> = User::find()
.select_only()
.column(user::Column::Name)
.into_tuple()
.all(db)
.await?;
// インデックス使用の最適化
let users = User::find()
.filter(user::Column::Email.starts_with("admin"))
.order_by_asc(user::Column::CreatedAt)
.limit(100)
.all(db)
.await?;
// ストリーミングクエリ(大量データ処理)
let mut stream = User::find().stream(db).await?;
while let Some(user) = stream.try_next().await? {
// 大量データを一件ずつ処理
process_user(&user).await;
}
// Raw SQLの使用(複雑なクエリ)
let custom_results: Vec<serde_json::Value> = db
.query_all(Statement::from_string(
DatabaseBackend::Postgres,
r#"
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.created_at > $1
GROUP BY u.id, u.name
ORDER BY post_count DESC
LIMIT 10
"#.to_owned(),
).values(vec![
(Utc::now() - Duration::days(30)).into()
]))
.await?
.into_iter()
.map(|row| row.try_get_by_index(0).unwrap())
.collect();
Ok(())
}
async fn process_user(user: &user::Model) {
// ユーザー処理ロジック
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}