SeaORM

SeaORMは「Rustのための非同期で動的なORM」として開発された、現代的なRustエコシステムで注目されているORM(Object-Relational Mapping)ライブラリです。非同期処理をネイティブサポートし、PostgreSQL、MySQL、SQLite対応の強力な型安全性を提供。SeaQLチームによって開発され、関数型リレーショナルマッピング(FRM)アプローチにより、コンパイル時の型チェックと優れたパフォーマンスを実現しています。Axum、Actix-web等の主要Webフレームワークとの統合も容易で、スキーマ駆動開発とコード生成によりRustの生産性を大幅に向上させます。

ORMRust非同期エンティティマイグレーション型安全

GitHub概要

SeaQL/sea-orm

🐚 An async & dynamic ORM for Rust

スター8,582
ウォッチ39
フォーク600
作成日:2021年2月9日
言語:Rust
ライセンス:Apache License 2.0

トピックス

databasehacktoberfestlocomariadbmysqlormpostgresrustsqlsqlitesqlxtokio

スター履歴

SeaQL/sea-orm Star History
データ取得日時: 2025/8/13 01:43

ライブラリ

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;
}