Rust ORMにおけるActive RecordとData Mapper
Emily Parker
Product Engineer · Leapcell

ORMアーキテクチャの理解:Sea-ORM vs Diesel
パフォーマンスとメモリ安全性で称賛されるRustプログラミング言語は、バックエンド開発で急速に支持を得ています。アプリケーションが複雑になるにつれて、データベースとの効果的な連携が最重要になります。オブジェクトリレーショナルマッパー(ORM)は、オブジェクト指向プログラミングパラダイムとリレーショナルデータベースの間のギャップを埋め、データの管理をより人間工学的な方法で提供します。しかし、すべてのORMが平等に作られているわけではなく、それらの基盤となるアーキテクチャ思想は、開発者がそれらとどのように対話するかに大きく影響を与える可能性があります。この記事では、Active RecordおよびData Mapperパターンの観点から、Rustの2つの主要なORMであるSea-ORMとDieselを比較検討します。
Active RecordとData Mapper ORMの選択は、単なる構文上の好み以上のものです。それはアプリケーション構造、テスト容易性、保守性に影響を与える決定です。それぞれの方法論の核となる原則を理解することは、開発者が特定のプロジェクトのニーズに最も適したツールを選択することを可能にします。この議論は、Rustエコシステム内のこれらのアーキテクチャパターンを解明し、それらの違いと強みを例示するための実践的な洞察とコード例を提供することを目的としています。
アーキテクチャ思想の解説
Sea-ORMとDieselを分析する前に、2つの基本的なORMアーキテクチャパターン、Active RecordとData Mapperを明確に理解しましょう。
Active Record
Martin Fowlerによって記述されたActive Recordパターンは、データと振る舞いを単一のオブジェクトにカプセル化します。各Active Recordオブジェクトは、データベーステーブルの行に直接対応します。これは、永続化(保存、更新、削除)および取得のメソッドが、通常、エンティティモデル自体で直接利用可能であることを意味します。「ドメインロジック」は、しばしばこれらのモデルオブジェクト内に直接配置されます。
Active Recordの主な特徴:
- 直接マッピング: モデルクラスとデータベーステーブルの間に、しばしば1対1の強力な対応関係があります。
- 自己完結型エンティティ: モデルオブジェクトは、それ自体の永続化に責任を持ちます。
- CRUD操作のシンプルさ: 基本的なデータ操作の定型コードが少なくなる傾向があります。
- 結合の可能性: ビジネスロジックとデータアクセスロジックが、同じクラス内に緊密に絡み合う可能性があります。
Data Mapper
対照的に、Data Mapperパターンは、メモリ上のオブジェクトとデータベースの間に抽象化レイヤーを導入します。このマッパーは、通常、別のクラスまたは一連の関数であり、オブジェクトとデータベース間、およびその逆へのデータの転送を担当します。したがって、ドメインオブジェクト(エンティティ)は、データベーススキーマやそれらがどのように永続化されるかについて何も知る必要がなくなります。
Data Mapperの主な特徴:
- 関心の分離: ドメインオブジェクトとデータアクセスロジックの明確な区別。
- 永続化の無視: ドメインオブジェクトには、データベース固有のコードは含まれません。
- 柔軟性: 複雑なデータベーススキーマをオブジェクトモデルにマッピングしたり、永続化メカニズムを切り替えたりするのが容易です。
- 複雑さの増加: 追加のマッピングレイヤーのために、特に単純なアプリケーションでは、より多くの定型コードが必要になる場合があります。
Sea-ORM:Active Recordアプローチ
Sea-ORM(SeaQueryおよびSeaSchemaの背後にあるのと同じチームであるSeaQLチームによって開発)は、RustでActive Recordパターンを具現化しています。クエリの構築とデータベースとの対話のための流暢なAPIを提供し、Rust構造体からデータベーススキーマを導出することに重点を置いています。
単純な Post の例で説明しましょう。
// entities/src/post.rs use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "posts")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub title: String, pub content: String, pub created_at: DateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {}
Sea-ORMでは、Model構造体はデータ自体を表し、ActiveModelはレコードの作成と更新に使用される変更可能なバージョンです。DeriveEntityModelマクロは、Active Record操作に必要な定型コードの多くを生成します。
Sea-ORMでの永続化:
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use super::entities::post; // entities/src/post.rs を想定 async fn create_and_save_post(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> { let new_post = post::ActiveModel { title: Set("My First Post".to_owned()), content: Set("This is the content of my first post.".to_owned()), created_at: Set(chrono::Utc::now()), ..Default::default() // 他のフィールドのデフォルト値を埋める }; let post_result = new_post.insert(db).await?; println!("Created post: {:?}", post_result); Ok(()) } async fn find_and_update_post(db: &DatabaseConnection, post_id: i32) -> Result<(), sea_orm::DbErr> { let mut post: post::ActiveModel = post::Entity::find_by_id(post_id) .one(db) .await?; .ok_or(sea_orm::DbErr::RecordNotFound("Post not found".to_string()))?; .into_active_model(); post.title = Set("Updated Title".to_owned()); post.update(db).await?; println!("Updated post with ID {}: {:?}", post_id, post); Ok(()) }
ActiveModelインスタンス上で直接 insert と update メソッドが呼び出されていることに注意してください。これは、オブジェクト自体が自身の永続性を認識しているActive Recordの原則を示しています。Sea-ORMは、単純なCRUDケースではしばしば少ないセットアップで、これらの操作を実行するための非常に人間工学的な方法を提供します。
Sea-ORMのユースケース:
- 単純なデータベーススキーマとドメインモデルへの直接マッピングを持つアプリケーション。
- 基本的な操作の高速開発が最優先されるプロトタイプおよびアプリケーション。
- オブジェクトモデルにおけるデータと振る舞いの緊密な結合が許容されるか望ましいシナリオ。
Diesel:Data Mapperアプローチ
Rustコミュニティで長年広く使用されているORMであるDieselは、Data Mapperパターンを採用しています。Rust構造体(ドメインモデルを表す)をデータベースと対話するためのロジックから分離します。Dieselは、強力なマクロシステムを通じて、スキーマを認識するクエリビルダーを生成し、コンパイル時にクエリの正確性を保証する強力な型システムを実現します。
Dieselで同じ Post の例を考えてみましょう。まず、Dieselの table! マクロを使用するか、コード生成ツール(diesel print-schema)を通じてデータベーススキーマを定義します。
// src/schema.rs (diesel print-schemaによって生成) diesel::table! { posts (id) { id -> Int4, title -> Varchar, content -> Text, created_at -> Timestamptz, } }
次に、Post エンティティを表すRust構造体を定義します。この構造体は「永続化を無視」します。
// src/models.rs use diesel::{Queryable, Insertable}; use chrono::NaiveDateTime; use super::schema::posts; #[derive(Queryable, Debug, PartialEq, Eq)] pub struct Post { pub id: i32, pub title: String, pub content: String, pub created_at: NaiveDateTime, } #[derive(Insertable)] #[diesel(table_name = posts)] pub struct NewPost { pub title: String, pub content: String, pub created_at: NaiveDateTime, }
Post および NewPost 構造体には、それ自体を保存または更新するメソッドが含まれていないことに注意してください。これらの操作は、Dieselのクエリビルダーによって処理されます。
Dieselでの永続化:
use diesel::prelude::*; use diesel::PgConnection; // または好みのデータベース use crate::models::{Post, NewPost}; use crate::schema::posts::dsl.*; // テーブルDSLをインポート use chrono::Utc; fn create_and_save_post(conn: &mut PgConnection) -> Result<Post, diesel::result::Error> { let new_post = NewPost { title: "My First Post".to_owned(), content: "This is the content of my first post.".to_owned(), created_at: Utc::now().naive_utc(), }; diesel::insert_into(posts) .values(&new_post) .get_result(conn) // クエリを実行し、挿入されたオブジェクトを返す } fn find_and_update_post(conn: &mut PgConnection, post_id: i32) -> Result<Post, diesel::result::Error> { let target_post = posts.filter(id.eq(post_id)); let updated_post = diesel::update(target_post) .set(title.eq("Updated Title")) .get_result(conn)?; Ok(updated_post) }
Dieselでは、insert_into と update は、データベース接続を受け取り、クエリを構築する関数です。Post および NewPost 構造体は厳密にデータを表し、diesel::insert_into、diesel::update、および filter 関数は、オブジェクトとデータベースの間を仲介するマッパーです。この明示的な分離により、より優れた制御が可能になり、複雑なクエリとマッピングが可能になります。
Dieselのユースケース:
- ドメインロジックとデータ永続化の間の厳密な関心の分離を必要とするアプリケーション。
- 複雑なクエリ、カスタムSQL、または高度に最適化されたデータベースインタラクションが頻繁に必要なプロジェクト。
- クエリの正確性と型安全性に関する堅牢なコンパイル時保証が必要なアプリケーション。
- テスト容易性とモジュール性が重要な、大規模で保守可能なコードベースを構築する場合。
アーキテクチャの比較
| 特徴 | Sea-ORM (Active Record) | Diesel (Data Mapper) |
|---|---|---|
| 哲学 | オブジェクトは自身を永続化する方法を知っている。 | 別のマッパーがオブジェクトとデータベースの変換を処理する。 |
| エンティティ設計 | 状態と振る舞いのためのModelとActiveModel。 | データの純粋な構造体(永続化を無視)。 |
| APIスタイル | エンティティインスタンスでの流暢なメソッドチェイン(.insert())。 | テーブルDSL(diesel::insert_into())で操作されるクエリビルダー。 |
| 結合 | オブジェクトと永続化の間の結合度が高い。 | 結合度が低い。ドメインオブジェクトは永続化に依存しない。 |
| 定型コード | エンティティでのマクロ導出により、基本的なCRUD操作が少ない。 | 基本的なCRUD操作ではより多いが、細かい制御が可能。 |
| テスト | ドメインロジックを分離してテストするのが難しい場合がある。 | ドメインロジックを独立してテストするのが容易。 |
| クエリの柔軟性 | 一般的なクエリには適している。生のSQLも使用可能。 | 非常に柔軟で堅牢なクエリビルダー。カスタムSQLもサポート。 |
| スキーマ定義 | Rust構造体から導出される。 | table!マクロまたはprint-schema(データベースファースト)によって定義される。 |
| コンパイル時チェック | エンティティの妥当性に重点を置く。 | クエリの正確性と型の強力なコンパイル時チェック。 |
結論
Sea-ORMとDieselはどちらもRustでのデータベースインタラクションのための説得力のあるソリューションを提供しており、それぞれ異なる好みとプロジェクト要件に合わせて最適化されています。Active Recordパターンを持つSea-ORMは、永続化ロジックをモデルに直接埋め込むことで基本的なCRUD操作を簡素化し、迅速な開発と単純なデータモデルを持つアプリケーションに最適です。Data Mapperパターンを採用したDieselは、データベースインタラクションの細かい制御、広範なカスタムクエリ、および厳密な関心の分離を要求する複雑なアプリケーションに理想的な、堅牢で型安全性が高く、分離されたアプローチを提供します。
最終的な選択は、プロジェクトの規模、複雑さ、そしてチームのアーキテクチャの好みに依存します。Active Recordエンティティの固有のシンプルさを求めるか、Data Mapperの強力な抽象化を求めるかにかかわらず、RustのORMエコシステムは、パフォーマンスが高く信頼性の高いアプリケーションを構築するための成熟した有能なオプションを提供します。

