Diesel

DieselはRustエコシステムで最も成熟した安全で拡張可能なORMおよびクエリビルダーです。コンパイル時型チェック、広範囲なクエリ検証、確立されたパフォーマンスを特徴とし、Rustの所有権システムと型システムを最大限活用してデータベース操作の安全性を保証します。SQLiteアクセスの薄いラッパーから複雑な結合クエリまで、幅広いユースケースに対応し、実行時エラーを排除してコンパイル時にSQL構文とスキーマの整合性を検証する革新的なアプローチを提供しています。

ORMRust型安全クエリビルダーコンパイル時検証SQLitePostgreSQLMySQL

GitHub概要

diesel-rs/diesel

A safe, extensible ORM and Query Builder for Rust

ホームページ:https://diesel.rs
スター13,477
ウォッチ110
フォーク1,137
作成日:2015年8月29日
言語:Rust
ライセンス:Apache License 2.0

トピックス

mysqlormpostgresqlquery-builderrustsqlite

スター履歴

diesel-rs/diesel Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

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, "投稿者");
    }
}