Diesel
DieselはRustエコシステムで最も成熟した安全で拡張可能なORMおよびクエリビルダーです。コンパイル時型チェック、広範囲なクエリ検証、確立されたパフォーマンスを特徴とし、Rustの所有権システムと型システムを最大限活用してデータベース操作の安全性を保証します。SQLiteアクセスの薄いラッパーから複雑な結合クエリまで、幅広いユースケースに対応し、実行時エラーを排除してコンパイル時にSQL構文とスキーマの整合性を検証する革新的なアプローチを提供しています。
GitHub概要
トピックス
スター履歴
ライブラリ
Diesel
概要
DieselはRustエコシステムで最も成熟した安全で拡張可能なORMおよびクエリビルダーです。コンパイル時型チェック、広範囲なクエリ検証、確立されたパフォーマンスを特徴とし、Rustの所有権システムと型システムを最大限活用してデータベース操作の安全性を保証します。SQLiteアクセスの薄いラッパーから複雑な結合クエリまで、幅広いユースケースに対応し、実行時エラーを排除してコンパイル時にSQL構文とスキーマの整合性を検証する革新的なアプローチを提供しています。
詳細
Diesel 2025年版は、Rust ORM分野の老舗王者として確固たる地位を維持し続けています。diesel-asyncでモダンな非同期サポートを提供開始し、tokio/axum系統の現代的なWebサービス開発にも対応できるようになりました。PostgreSQL、MySQL、SQLiteの3つの主要データベースを完全サポートし、マイグレーション管理、コネクションプーリング、バッチ操作、トランザクション処理などエンタープライズ開発に必要な機能を包括的に提供します。Rustの強力な型システムにより、実行時エラーを大幅に削減し、クエリの型安全性を保証する設計は、他言語のORMでは実現困難な堅牢性を実現しています。
主な特徴
- コンパイル時型チェック: SQLクエリとスキーマの整合性をコンパイル時に検証
- ゼロコスト抽象化: Rustのゼロコスト抽象化によるパフォーマンス最適化
- 広範囲なデータベースサポート: PostgreSQL、MySQL、SQLite対応
- マイグレーション管理: バージョン管理されたスキーマ変更の自動適用
- クエリビルダー: 型安全でSQL注入攻撃に対する完全防御
- 非同期サポート: diesel-asyncによる現代的な非同期パフォーマンス
メリット・デメリット
メリット
- Rustのコンパイル時安全性によるSQL実行時エラーの完全防止
- 高度な型推論によるボイラープレートコードの大幅削減
- diesel CLIツールによる効率的なマイグレーションとスキーマ管理
- 複雑な結合クエリとサブクエリのタイプセーフな実装が可能
- ゼロコスト抽象化による他ORMを上回るパフォーマンス
- 豊富なコミュニティサポートと詳細なドキュメント
- PostgreSQL固有機能(配列、JSON、カスタム型)の完全サポート
デメリット
- Rust初心者には学習コストが高く、複雑な型システムの理解が必要
- スキーマファーストの設計により、動的クエリ生成が複雑
- コンパイル時間がクエリ複雑性に比例して増加する傾向
- 他言語エコシステムとの統合が困難でRustオンリー環境が前提
- ドキュメント生成ツールの設定と保守に追加工数が必要
- 新しいSQL機能への対応に時間がかかる場合がある
参考ページ
書き方の例
プロジェクトセットアップとCargo設定
# Cargo.toml
[dependencies]
diesel = { version = "2.1", features = ["postgres", "chrono", "uuid"] }
diesel_migrations = "2.1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
tokio = { version = "1.0", features = ["full"] }
# 非同期サポートが必要な場合
diesel-async = { version = "0.4", features = ["postgres", "deadpool"] }
deadpool = { version = "0.10", features = ["managed"] }
[dev-dependencies]
diesel_cli = "2.1"
# Diesel CLIのインストール
cargo install diesel_cli --no-default-features --features postgres
# プロジェクトの初期化
diesel setup
# マイグレーションの作成
diesel migration generate create_users_table
# マイグレーションの実行
diesel migration run
# スキーマファイルの生成
diesel print-schema > src/schema.rs
データベーススキーマ定義とマイグレーション
-- migrations/2024_01_01_000000_create_users_table/up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR NOT NULL,
email VARCHAR UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
-- migrations/2024_01_01_000000_create_users_table/down.sql
DROP TABLE users;
-- migrations/2024_01_02_000000_create_posts_table/up.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR NOT NULL,
content TEXT NOT NULL,
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_published ON posts(published);
-- migrations/2024_01_02_000000_create_posts_table/down.sql
DROP TABLE posts;
// src/schema.rs (diesel print-schemaで自動生成)
diesel::table! {
users (id) {
id -> Uuid,
name -> Varchar,
email -> Varchar,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::table! {
posts (id) {
id -> Uuid,
title -> Varchar,
content -> Text,
author_id -> Uuid,
published -> Bool,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
diesel::joinable!(posts -> users (author_id));
diesel::allow_tables_to_appear_in_same_query!(
users,
posts,
);
モデル定義とCRUD操作
// src/models.rs
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::schema::{users, posts};
// ユーザーモデル(クエリ用)
#[derive(Queryable, Selectable, Serialize, Deserialize, Debug, Clone)]
#[diesel(table_name = users)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// 新規ユーザー作成用
#[derive(Insertable, Deserialize)]
#[diesel(table_name = users)]
pub struct NewUser {
pub name: String,
pub email: String,
}
// ユーザー更新用
#[derive(AsChangeset, Deserialize)]
#[diesel(table_name = users)]
pub struct UpdateUser {
pub name: Option<String>,
pub email: Option<String>,
pub updated_at: DateTime<Utc>,
}
// 投稿モデル(クエリ用)
#[derive(Queryable, Selectable, Serialize, Deserialize, Debug, Clone)]
#[diesel(table_name = posts)]
pub struct Post {
pub id: Uuid,
pub title: String,
pub content: String,
pub author_id: Uuid,
pub published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// 新規投稿作成用
#[derive(Insertable, Deserialize)]
#[diesel(table_name = posts)]
pub struct NewPost {
pub title: String,
pub content: String,
pub author_id: Uuid,
pub published: Option<bool>,
}
// 投稿更新用
#[derive(AsChangeset, Deserialize)]
#[diesel(table_name = posts)]
pub struct UpdatePost {
pub title: Option<String>,
pub content: Option<String>,
pub published: Option<bool>,
pub updated_at: DateTime<Utc>,
}
// 投稿と著者の結合クエリ結果
#[derive(Queryable, Selectable, Serialize, Debug)]
pub struct PostWithAuthor {
#[diesel(embed)]
pub post: Post,
#[diesel(embed)]
pub author: User,
}
// CRUD操作の実装
impl User {
// 全ユーザー取得
pub fn get_all(conn: &mut PgConnection) -> QueryResult<Vec<User>> {
users::table
.select(User::as_select())
.order(users::created_at.desc())
.load(conn)
}
// ID指定でユーザー取得
pub fn get_by_id(conn: &mut PgConnection, user_id: Uuid) -> QueryResult<Option<User>> {
users::table
.find(user_id)
.select(User::as_select())
.first(conn)
.optional()
}
// メールアドレスでユーザー取得
pub fn get_by_email(conn: &mut PgConnection, user_email: &str) -> QueryResult<Option<User>> {
users::table
.filter(users::email.eq(user_email))
.select(User::as_select())
.first(conn)
.optional()
}
// 新規ユーザー作成
pub fn create(conn: &mut PgConnection, new_user: NewUser) -> QueryResult<User> {
diesel::insert_into(users::table)
.values(&new_user)
.returning(User::as_returning())
.get_result(conn)
}
// ユーザー更新
pub fn update(conn: &mut PgConnection, user_id: Uuid, update_user: UpdateUser) -> QueryResult<User> {
diesel::update(users::table.find(user_id))
.set(&update_user)
.returning(User::as_returning())
.get_result(conn)
}
// ユーザー削除
pub fn delete(conn: &mut PgConnection, user_id: Uuid) -> QueryResult<usize> {
diesel::delete(users::table.find(user_id))
.execute(conn)
}
// ユーザーの投稿一覧取得
pub fn get_posts(&self, conn: &mut PgConnection) -> QueryResult<Vec<Post>> {
Post::belonging_to(self)
.select(Post::as_select())
.order(posts::created_at.desc())
.load(conn)
}
}
impl Post {
// 全投稿取得(公開済みのみ)
pub fn get_published(conn: &mut PgConnection) -> QueryResult<Vec<Post>> {
posts::table
.filter(posts::published.eq(true))
.select(Post::as_select())
.order(posts::created_at.desc())
.load(conn)
}
// 投稿と著者情報を同時取得
pub fn get_with_authors(conn: &mut PgConnection) -> QueryResult<Vec<PostWithAuthor>> {
posts::table
.inner_join(users::table)
.filter(posts::published.eq(true))
.select((Post::as_select(), User::as_select()))
.order(posts::created_at.desc())
.load::<(Post, User)>(conn)?
.into_iter()
.map(|(post, author)| Ok(PostWithAuthor { post, author }))
.collect()
}
// 新規投稿作成
pub fn create(conn: &mut PgConnection, new_post: NewPost) -> QueryResult<Post> {
diesel::insert_into(posts::table)
.values(&new_post)
.returning(Post::as_returning())
.get_result(conn)
}
// 投稿更新
pub fn update(conn: &mut PgConnection, post_id: Uuid, update_post: UpdatePost) -> QueryResult<Post> {
diesel::update(posts::table.find(post_id))
.set(&update_post)
.returning(Post::as_returning())
.get_result(conn)
}
}
複雑なクエリとリレーションシップ
// src/queries.rs
use diesel::prelude::*;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::models::{User, Post, PostWithAuthor};
use crate::schema::{users, posts};
// 複雑な検索クエリ
pub fn search_posts_by_keyword(
conn: &mut PgConnection,
keyword: &str,
limit: i64,
offset: i64,
) -> QueryResult<Vec<PostWithAuthor>> {
posts::table
.inner_join(users::table)
.filter(
posts::title.ilike(format!("%{}%", keyword))
.or(posts::content.ilike(format!("%{}%", keyword)))
)
.filter(posts::published.eq(true))
.select((Post::as_select(), User::as_select()))
.order(posts::created_at.desc())
.limit(limit)
.offset(offset)
.load::<(Post, User)>(conn)?
.into_iter()
.map(|(post, author)| Ok(PostWithAuthor { post, author }))
.collect()
}
// サブクエリを使用した複雑なクエリ例
pub fn get_recent_active_authors(
conn: &mut PgConnection,
days: i32,
) -> QueryResult<Vec<User>> {
use diesel::dsl::*;
let recent_date = Utc::now().naive_utc() - chrono::Duration::days(days as i64);
// 最近投稿したユーザーのサブクエリ
let recent_authors = posts::table
.filter(posts::created_at.gt(recent_date))
.filter(posts::published.eq(true))
.select(posts::author_id)
.distinct();
users::table
.filter(users::id.eq_any(recent_authors))
.select(User::as_select())
.order(users::name.asc())
.load(conn)
}
// 集約クエリの例
#[derive(QueryableByName, Debug)]
pub struct UserPostCount {
#[diesel(sql_type = diesel::sql_types::Uuid)]
pub user_id: Uuid,
#[diesel(sql_type = diesel::sql_types::Text)]
pub user_name: String,
#[diesel(sql_type = diesel::sql_types::BigInt)]
pub post_count: i64,
}
pub fn get_user_post_counts(conn: &mut PgConnection) -> QueryResult<Vec<UserPostCount>> {
diesel::sql_query(
"
SELECT
u.id as user_id,
u.name as user_name,
COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id AND p.published = true
GROUP BY u.id, u.name
ORDER BY post_count DESC, u.name ASC
"
).load(conn)
}
// バッチ操作の例
pub fn bulk_publish_posts(
conn: &mut PgConnection,
post_ids: Vec<Uuid>,
) -> QueryResult<usize> {
diesel::update(
posts::table.filter(posts::id.eq_any(post_ids))
)
.set((
posts::published.eq(true),
posts::updated_at.eq(Utc::now().naive_utc()),
))
.execute(conn)
}
// 条件付き削除
pub fn delete_old_unpublished_posts(
conn: &mut PgConnection,
days: i32,
) -> QueryResult<usize> {
let cutoff_date = Utc::now().naive_utc() - chrono::Duration::days(days as i64);
diesel::delete(
posts::table
.filter(posts::published.eq(false))
.filter(posts::created_at.lt(cutoff_date))
)
.execute(conn)
}
非同期対応とコネクションプール
// src/database.rs
use diesel_async::{AsyncPgConnection, AsyncConnection, pooled_connection::AsyncDieselConnectionManager};
use deadpool::managed::{Pool, Object};
use std::env;
pub type DbPool = Pool<AsyncDieselConnectionManager<AsyncPgConnection>>;
pub type DbConnection = Object<AsyncDieselConnectionManager<AsyncPgConnection>>;
// データベース接続プールの設定
pub async fn create_pool() -> DbPool {
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let manager = AsyncDieselConnectionManager::<AsyncPgConnection>::new(database_url);
Pool::builder(manager)
.max_size(16)
.build()
.expect("Failed to create connection pool")
}
// 非同期CRUD操作
impl User {
pub async fn get_all_async(conn: &mut AsyncPgConnection) -> QueryResult<Vec<User>> {
use crate::schema::users::dsl::*;
users
.select(User::as_select())
.order(created_at.desc())
.load(conn)
.await
}
pub async fn create_async(conn: &mut AsyncPgConnection, new_user: NewUser) -> QueryResult<User> {
use crate::schema::users::dsl::*;
diesel::insert_into(users)
.values(&new_user)
.returning(User::as_returning())
.get_result(conn)
.await
}
pub async fn get_posts_async(&self, conn: &mut AsyncPgConnection) -> QueryResult<Vec<Post>> {
use crate::schema::posts::dsl::*;
Post::belonging_to(self)
.select(Post::as_select())
.order(created_at.desc())
.load(conn)
.await
}
}
// トランザクション処理
pub async fn create_user_with_post(
pool: &DbPool,
new_user: NewUser,
new_post: NewPost,
) -> Result<(User, Post), Box<dyn std::error::Error>> {
let mut conn = pool.get().await?;
conn.transaction::<_, Box<dyn std::error::Error>, _>(|conn| {
Box::pin(async move {
// ユーザー作成
let user = User::create_async(conn, new_user).await?;
// 投稿作成(ユーザーIDを設定)
let mut post_data = new_post;
post_data.author_id = user.id;
let post = Post::create_async(conn, post_data).await?;
Ok((user, post))
})
}).await
}
テストとモック
// src/test_helpers.rs
#[cfg(test)]
pub mod test_helpers {
use diesel::prelude::*;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use diesel::pg::PgConnection;
use std::env;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
pub fn establish_test_connection() -> PgConnection {
let database_url = env::var("TEST_DATABASE_URL")
.expect("TEST_DATABASE_URL must be set for tests");
let mut connection = PgConnection::establish(&database_url)
.expect("Failed to connect to test database");
// テスト用マイグレーション実行
connection.run_pending_migrations(MIGRATIONS)
.expect("Failed to run migrations");
connection
}
pub fn cleanup_database(conn: &mut PgConnection) {
use crate::schema::{posts, users};
diesel::delete(posts::table).execute(conn).unwrap();
diesel::delete(users::table).execute(conn).unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::*;
use crate::test_helpers::*;
#[test]
fn test_user_crud() {
let mut conn = establish_test_connection();
cleanup_database(&mut conn);
// ユーザー作成テスト
let new_user = NewUser {
name: "テストユーザー".to_string(),
email: "[email protected]".to_string(),
};
let created_user = User::create(&mut conn, new_user).unwrap();
assert_eq!(created_user.name, "テストユーザー");
assert_eq!(created_user.email, "[email protected]");
// ユーザー取得テスト
let found_user = User::get_by_id(&mut conn, created_user.id).unwrap().unwrap();
assert_eq!(found_user.id, created_user.id);
// ユーザー更新テスト
let update_data = UpdateUser {
name: Some("更新されたユーザー".to_string()),
email: None,
updated_at: chrono::Utc::now(),
};
let updated_user = User::update(&mut conn, created_user.id, update_data).unwrap();
assert_eq!(updated_user.name, "更新されたユーザー");
// ユーザー削除テスト
let deleted_count = User::delete(&mut conn, created_user.id).unwrap();
assert_eq!(deleted_count, 1);
let not_found = User::get_by_id(&mut conn, created_user.id).unwrap();
assert!(not_found.is_none());
}
#[test]
fn test_post_with_author() {
let mut conn = establish_test_connection();
cleanup_database(&mut conn);
// テストユーザー作成
let new_user = NewUser {
name: "投稿者".to_string(),
email: "[email protected]".to_string(),
};
let author = User::create(&mut conn, new_user).unwrap();
// テスト投稿作成
let new_post = NewPost {
title: "テスト投稿".to_string(),
content: "これはテスト投稿の内容です。".to_string(),
author_id: author.id,
published: Some(true),
};
let post = Post::create(&mut conn, new_post).unwrap();
// 投稿と著者情報の結合取得テスト
let posts_with_authors = Post::get_with_authors(&mut conn).unwrap();
assert_eq!(posts_with_authors.len(), 1);
assert_eq!(posts_with_authors[0].post.title, "テスト投稿");
assert_eq!(posts_with_authors[0].author.name, "投稿者");
}
}