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の利点を活かしたパフォーマンス重視のデータアクセス層を構築できます。
GitHub概要
トピックス
スター履歴
ライブラリ
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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, ¶ms)?;
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");
}
}