Rustorm

Rustormは「SQL中心のORMライブラリで、データベース型から適切なRust型への変換の容易さに焦点を当てた」Rust専用のObject-Relational Mapping(ORM)ライブラリです。SQLの抽象化よりもSQL操作に重点を置き、PostgreSQLとSQLiteデータベースをサポートする軽量で実用的なアプローチを提供します。derive マクロ(ToDao、FromDao、ToColumnNames、ToTableName)を活用した構造体定義により、型安全なデータベース操作を実現し、RustのMemory SafetyとZero-cost Abstractionの利点を活かしたパフォーマンス重視のデータアクセス層を構築できます。

RustDatabaseORMSQLPostgreSQLSQLiteType Conversion

GitHub概要

ivanceras/rustorm

an orm for rust

スター250
ウォッチ11
フォーク23
作成日:2017年12月13日
言語:Rust
ライセンス:MIT License

トピックス

databaseormpostgresqlrustsqlite

スター履歴

ivanceras/rustorm Star History
データ取得日時: 2025/7/19 09:29

ライブラリ

Rustorm

概要

Rustormは「SQL中心のORMライブラリで、データベース型から適切なRust型への変換の容易さに焦点を当てた」Rust専用のObject-Relational Mapping(ORM)ライブラリです。SQLの抽象化よりもSQL操作に重点を置き、PostgreSQLとSQLiteデータベースをサポートする軽量で実用的なアプローチを提供します。derive マクロ(ToDao、FromDao、ToColumnNames、ToTableName)を活用した構造体定義により、型安全なデータベース操作を実現し、RustのMemory SafetyとZero-cost Abstractionの利点を活かしたパフォーマンス重視のデータアクセス層を構築できます。

詳細

Rustorm 2025年版は、Rust 1.75以降の最新機能(async/await、Generic Associated Types、const generics等)を活用し、よりエルゴノミックで型安全なデータベースアクセスを提供します。SQL中心のアプローチにより、複雑なクエリを直接記述可能で、Rustの所有権システムと相性の良い設計により、メモリ効率と実行時パフォーマンスを最大化。プールベースの接続管理、トランザクション制御、自動型変換により、高負荷なWebアプリケーションや組み込みシステムでの使用に適した堅牢なデータ層を構築できます。また、serde統合による柔軟なシリアライゼーション、マイグレーション機能、テスト支援ユーティリティにより、プロダクション環境での運用を強力にサポートします。

主な特徴

  • SQL中心設計: 抽象化を最小限に抑えたSQL直接記述によるクエリ制御
  • 型変換フォーカス: データベース型からRust型への自動変換機能
  • derive マクロ: ToDao、FromDao等のマクロによる定型コード削減
  • 複数DB対応: PostgreSQLとSQLiteの両方をサポート
  • プール管理: 効率的なコネクションプール機能
  • 軽量設計: 最小限の依存関係で高パフォーマンス

メリット・デメリット

メリット

  • SQL知識を活かしつつRustの型安全性の恩恵を受けられる
  • 軽量で学習コストが低く、他のRust ORMより導入が容易
  • 複雑なクエリを直接SQLで記述できる透明性の高さ
  • Rustの所有権システムと相性が良くメモリ効率が高い
  • PostgreSQLとSQLiteの両方に対応した柔軟性
  • derive マクロによる定型コード削減と開発効率向上

デメリット

  • Rust専用でクロスプラットフォーム開発には使用不可
  • DieselやSeaORMに比べて機能が限定的
  • 高度なORM機能(リレーション管理等)は手動実装が必要
  • コミュニティサイズが小さく、ドキュメントやサンプルが限定的
  • NoSQL系データベースには対応していない
  • 複雑なスキーマ設計では抽象化の恩恵が少ない

参考ページ

書き方の例

セットアップ

# Cargo.toml
[dependencies]
rustorm = "0.20"
rustorm_dao = "0.20"

# PostgreSQL使用時
postgres = "0.19"
tokio-postgres = "0.7"

# SQLite使用時
rusqlite = "0.29"

# 非同期ランタイム
tokio = { version = "1.0", features = ["full"] }

# シリアライゼーション
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }

[dev-dependencies]
tokio-test = "0.4"
// src/lib.rs
use rustorm::{Pool, DbError, Dao, FromDao, ToDao, ToColumnNames, ToTableName};
use rustorm::table::Table;
use rustorm::column::Column;
use rustorm::value::Value;
use std::collections::HashMap;

// データベース接続の設定
pub fn establish_connection() -> Result<Pool, DbError> {
    // PostgreSQL接続文字列
    let postgres_url = "postgres://username:password@localhost/mydatabase";
    
    // SQLite接続文字列
    let sqlite_url = "sqlite://./mydatabase.db";
    
    // 接続プールの作成
    Pool::new(&postgres_url)
}

pub fn establish_sqlite_connection() -> Result<Pool, DbError> {
    let sqlite_url = "sqlite://./mydatabase.db";
    Pool::new(&sqlite_url)
}

エンティティ定義

use rustorm::{FromDao, ToDao, ToColumnNames, ToTableName};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;

// 基本エンティティ
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "users"]
pub struct User {
    pub id: Option<i32>,
    pub username: String,
    pub email: String,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
    pub is_active: bool,
}

impl User {
    pub fn new(username: String, email: String) -> Self {
        User {
            id: None,
            username,
            email,
            created_at: Some(Utc::now()),
            updated_at: None,
            is_active: true,
        }
    }
}

// 関連エンティティ
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "posts"]
pub struct Post {
    pub id: Option<i32>,
    pub user_id: i32,
    pub title: String,
    pub content: String,
    pub published: bool,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

impl Post {
    pub fn new(user_id: i32, title: String, content: String) -> Self {
        Post {
            id: None,
            user_id,
            title,
            content,
            published: false,
            created_at: Some(Utc::now()),
            updated_at: None,
        }
    }
}

// UUIDを使用したエンティティ
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "sessions"]
pub struct Session {
    pub id: Uuid,
    pub user_id: i32,
    pub token: String,
    pub expires_at: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
}

impl Session {
    pub fn new(user_id: i32, token: String, expires_at: DateTime<Utc>) -> Self {
        Session {
            id: Uuid::new_v4(),
            user_id,
            token,
            expires_at,
            created_at: Utc::now(),
        }
    }
}

// 結合データ用の構造体
#[derive(Debug, Clone, Serialize, Deserialize, FromDao)]
pub struct UserWithPostCount {
    pub id: i32,
    pub username: String,
    pub email: String,
    pub post_count: i64,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromDao)]
pub struct PostWithUser {
    pub post_id: i32,
    pub title: String,
    pub content: String,
    pub published: bool,
    pub post_created_at: DateTime<Utc>,
    pub user_id: i32,
    pub username: String,
    pub user_email: String,
}

基本的なCRUD操作

use rustorm::{Pool, DbError, Dao};
use crate::{User, Post, Session};

pub struct UserRepository {
    pool: Pool,
}

impl UserRepository {
    pub fn new(pool: Pool) -> Self {
        UserRepository { pool }
    }
    
    // ユーザー作成
    pub fn create_user(&self, mut user: User) -> Result<User, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            INSERT INTO users (username, email, created_at, is_active)
            VALUES ($1, $2, $3, $4)
            RETURNING id, username, email, created_at, updated_at, is_active
        "#;
        
        let params = vec![
            user.username.clone().into(),
            user.email.clone().into(),
            user.created_at.into(),
            user.is_active.into(),
        ];
        
        let result = db.execute_sql_with_return(&sql, &params)?;
        
        if let Some(row) = result.iter().next() {
            let created_user: User = User::from_dao(&row)?;
            Ok(created_user)
        } else {
            Err(DbError::NoRecord)
        }
    }
    
    // ユーザー取得(ID指定)
    pub fn get_user_by_id(&self, user_id: i32) -> Result<Option<User>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users WHERE id = $1";
        let params = vec![user_id.into()];
        
        let result = db.execute_sql_with_return(&sql, &params)?;
        
        if let Some(row) = result.iter().next() {
            let user: User = User::from_dao(&row)?;
            Ok(Some(user))
        } else {
            Ok(None)
        }
    }
    
    // ユーザー取得(ユーザー名指定)
    pub fn get_user_by_username(&self, username: &str) -> Result<Option<User>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users WHERE username = $1";
        let params = vec![username.into()];
        
        let result = db.execute_sql_with_return(&sql, &params)?;
        
        if let Some(row) = result.iter().next() {
            let user: User = User::from_dao(&row)?;
            Ok(Some(user))
        } else {
            Ok(None)
        }
    }
    
    // 全ユーザー取得
    pub fn get_all_users(&self) -> Result<Vec<User>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users ORDER BY created_at DESC";
        let result = db.execute_sql_with_return(&sql, &[])?;
        
        let mut users = Vec::new();
        for row in result.iter() {
            let user: User = User::from_dao(&row)?;
            users.push(user);
        }
        
        Ok(users)
    }
    
    // ユーザー更新
    pub fn update_user(&self, user: &User) -> Result<(), DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            UPDATE users 
            SET username = $1, email = $2, updated_at = $3, is_active = $4
            WHERE id = $5
        "#;
        
        let params = vec![
            user.username.clone().into(),
            user.email.clone().into(),
            Some(Utc::now()).into(),
            user.is_active.into(),
            user.id.into(),
        ];
        
        db.execute_sql_with_return(&sql, &params)?;
        Ok(())
    }
    
    // ユーザー削除
    pub fn delete_user(&self, user_id: i32) -> Result<(), DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = "DELETE FROM users WHERE id = $1";
        let params = vec![user_id.into()];
        
        db.execute_sql_with_return(&sql, &params)?;
        Ok(())
    }
    
    // アクティブユーザー検索
    pub fn get_active_users(&self) -> Result<Vec<User>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            SELECT id, username, email, created_at, updated_at, is_active 
            FROM users 
            WHERE is_active = true 
            ORDER BY username ASC
        "#;
        
        let result = db.execute_sql_with_return(&sql, &[])?;
        
        let mut users = Vec::new();
        for row in result.iter() {
            let user: User = User::from_dao(&row)?;
            users.push(user);
        }
        
        Ok(users)
    }
}

複雑なクエリと結合

use crate::{UserWithPostCount, PostWithUser};

impl UserRepository {
    // ユーザーと投稿数の取得
    pub fn get_users_with_post_count(&self) -> Result<Vec<UserWithPostCount>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            SELECT 
                u.id,
                u.username,
                u.email,
                u.created_at,
                COUNT(p.id) as post_count
            FROM users u
            LEFT JOIN posts p ON u.id = p.user_id
            WHERE u.is_active = true
            GROUP BY u.id, u.username, u.email, u.created_at
            ORDER BY post_count DESC, u.username ASC
        "#;
        
        let result = db.execute_sql_with_return(&sql, &[])?;
        
        let mut users_with_counts = Vec::new();
        for row in result.iter() {
            let user_with_count: UserWithPostCount = UserWithPostCount::from_dao(&row)?;
            users_with_counts.push(user_with_count);
        }
        
        Ok(users_with_counts)
    }
    
    // 投稿とユーザー情報の結合取得
    pub fn get_posts_with_users(&self, limit: i32) -> Result<Vec<PostWithUser>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            SELECT 
                p.id as post_id,
                p.title,
                p.content,
                p.published,
                p.created_at as post_created_at,
                u.id as user_id,
                u.username,
                u.email as user_email
            FROM posts p
            INNER JOIN users u ON p.user_id = u.id
            WHERE p.published = true AND u.is_active = true
            ORDER BY p.created_at DESC
            LIMIT $1
        "#;
        
        let params = vec![limit.into()];
        let result = db.execute_sql_with_return(&sql, &params)?;
        
        let mut posts_with_users = Vec::new();
        for row in result.iter() {
            let post_with_user: PostWithUser = PostWithUser::from_dao(&row)?;
            posts_with_users.push(post_with_user);
        }
        
        Ok(posts_with_users)
    }
    
    // 検索機能
    pub fn search_users(&self, search_term: &str) -> Result<Vec<User>, DbError> {
        let mut db = self.pool.connect()?;
        
        let sql = r#"
            SELECT id, username, email, created_at, updated_at, is_active 
            FROM users 
            WHERE (username ILIKE $1 OR email ILIKE $1) 
            AND is_active = true
            ORDER BY username ASC
        "#;
        
        let search_pattern = format!("%{}%", search_term);
        let params = vec![search_pattern.into()];
        
        let result = db.execute_sql_with_return(&sql, &params)?;
        
        let mut users = Vec::new();
        for row in result.iter() {
            let user: User = User::from_dao(&row)?;
            users.push(user);
        }
        
        Ok(users)
    }
}

トランザクション処理

use rustorm::Transaction;

impl UserRepository {
    // トランザクションを使用したユーザーと投稿の同時作成
    pub fn create_user_with_post(
        &self,
        mut user: User,
        mut post: Post,
    ) -> Result<(User, Post), DbError> {
        let mut db = self.pool.connect()?;
        let mut transaction = db.start_transaction()?;
        
        // ユーザー作成
        let user_sql = r#"
            INSERT INTO users (username, email, created_at, is_active)
            VALUES ($1, $2, $3, $4)
            RETURNING id, username, email, created_at, updated_at, is_active
        "#;
        
        let user_params = vec![
            user.username.clone().into(),
            user.email.clone().into(),
            user.created_at.into(),
            user.is_active.into(),
        ];
        
        let user_result = transaction.execute_sql_with_return(&user_sql, &user_params)?;
        let created_user: User = if let Some(row) = user_result.iter().next() {
            User::from_dao(&row)?
        } else {
            transaction.rollback()?;
            return Err(DbError::NoRecord);
        };
        
        // 投稿作成
        post.user_id = created_user.id.unwrap();
        let post_sql = r#"
            INSERT INTO posts (user_id, title, content, published, created_at)
            VALUES ($1, $2, $3, $4, $5)
            RETURNING id, user_id, title, content, published, created_at, updated_at
        "#;
        
        let post_params = vec![
            post.user_id.into(),
            post.title.clone().into(),
            post.content.clone().into(),
            post.published.into(),
            post.created_at.into(),
        ];
        
        let post_result = transaction.execute_sql_with_return(&post_sql, &post_params)?;
        let created_post: Post = if let Some(row) = post_result.iter().next() {
            Post::from_dao(&row)?
        } else {
            transaction.rollback()?;
            return Err(DbError::NoRecord);
        };
        
        // トランザクションコミット
        transaction.commit()?;
        
        Ok((created_user, created_post))
    }
    
    // 一括操作
    pub fn bulk_update_user_status(&self, user_ids: &[i32], is_active: bool) -> Result<usize, DbError> {
        let mut db = self.pool.connect()?;
        let mut transaction = db.start_transaction()?;
        
        let sql = "UPDATE users SET is_active = $1, updated_at = $2 WHERE id = ANY($3)";
        let user_ids_array = user_ids.iter().map(|&id| id.into()).collect::<Vec<_>>();
        
        let params = vec![
            is_active.into(),
            Some(Utc::now()).into(),
            user_ids_array.into(),
        ];
        
        let result = transaction.execute_sql_with_return(&sql, &params)?;
        let affected_rows = result.len();
        
        transaction.commit()?;
        Ok(affected_rows)
    }
}

テスト実装

#[cfg(test)]
mod tests {
    use super::*;
    use tokio_test;
    
    fn setup_test_db() -> Pool {
        // テスト用のインメモリSQLiteデータベース
        Pool::new("sqlite://:memory:").expect("Failed to create test database")
    }
    
    fn create_test_tables(pool: &Pool) -> Result<(), DbError> {
        let mut db = pool.connect()?;
        
        // テーブル作成
        let users_table = r#"
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT NOT NULL UNIQUE,
                email TEXT NOT NULL UNIQUE,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME,
                is_active BOOLEAN DEFAULT 1
            )
        "#;
        
        let posts_table = r#"
            CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                published BOOLEAN DEFAULT 0,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME,
                FOREIGN KEY (user_id) REFERENCES users (id)
            )
        "#;
        
        db.execute_sql_with_return(&users_table, &[])?;
        db.execute_sql_with_return(&posts_table, &[])?;
        
        Ok(())
    }
    
    #[tokio::test]
    async fn test_create_and_get_user() {
        let pool = setup_test_db();
        create_test_tables(&pool).unwrap();
        let repo = UserRepository::new(pool);
        
        // ユーザー作成
        let user = User::new("testuser".to_string(), "[email protected]".to_string());
        let created_user = repo.create_user(user).unwrap();
        
        assert!(created_user.id.is_some());
        assert_eq!(created_user.username, "testuser");
        assert_eq!(created_user.email, "[email protected]");
        assert!(created_user.is_active);
        
        // ユーザー取得
        let retrieved_user = repo.get_user_by_id(created_user.id.unwrap()).unwrap();
        assert!(retrieved_user.is_some());
        
        let retrieved_user = retrieved_user.unwrap();
        assert_eq!(retrieved_user.username, "testuser");
        assert_eq!(retrieved_user.email, "[email protected]");
    }
    
    #[tokio::test]
    async fn test_update_user() {
        let pool = setup_test_db();
        create_test_tables(&pool).unwrap();
        let repo = UserRepository::new(pool);
        
        // ユーザー作成
        let user = User::new("original".to_string(), "[email protected]".to_string());
        let mut created_user = repo.create_user(user).unwrap();
        
        // ユーザー更新
        created_user.username = "updated".to_string();
        created_user.email = "[email protected]".to_string();
        repo.update_user(&created_user).unwrap();
        
        // 更新確認
        let updated_user = repo.get_user_by_id(created_user.id.unwrap()).unwrap().unwrap();
        assert_eq!(updated_user.username, "updated");
        assert_eq!(updated_user.email, "[email protected]");
        assert!(updated_user.updated_at.is_some());
    }
    
    #[tokio::test]
    async fn test_user_search() {
        let pool = setup_test_db();
        create_test_tables(&pool).unwrap();
        let repo = UserRepository::new(pool);
        
        // テストデータ作成
        let users = vec![
            User::new("alice".to_string(), "[email protected]".to_string()),
            User::new("bob".to_string(), "[email protected]".to_string()),
            User::new("charlie".to_string(), "[email protected]".to_string()),
        ];
        
        for user in users {
            repo.create_user(user).unwrap();
        }
        
        // 検索テスト
        let search_results = repo.search_users("alice").unwrap();
        assert_eq!(search_results.len(), 1);
        assert_eq!(search_results[0].username, "alice");
        
        let email_search_results = repo.search_users("example.com").unwrap();
        assert_eq!(email_search_results.len(), 2);
    }
    
    #[tokio::test]
    async fn test_transaction() {
        let pool = setup_test_db();
        create_test_tables(&pool).unwrap();
        let repo = UserRepository::new(pool);
        
        let user = User::new("testuser".to_string(), "[email protected]".to_string());
        let post = Post::new(0, "Test Post".to_string(), "This is a test post.".to_string());
        
        let (created_user, created_post) = repo.create_user_with_post(user, post).unwrap();
        
        assert!(created_user.id.is_some());
        assert!(created_post.id.is_some());
        assert_eq!(created_post.user_id, created_user.id.unwrap());
        assert_eq!(created_post.title, "Test Post");
    }
}