Prisma Client Rust
Prisma Client Rustは「タイプセーフで自動生成されるRust用クエリビルダー」として開発された、PrismaエコシステムをRustで活用するためのORM(Object-Relational Mapping)crateです。一貫性のある理解しやすいAPIをPrismaの強力なデータベース技術の上に提供し、Diesel、SeaORM、SQLxのような既存のORMやツールの代替手段として機能。Rustアプリケーションにおいて、Prismaエコシステム全体を活用しながら、フロントエンド(Tauri等)からサーバーサイドまで一貫したタイプセーフなデータベースアクセスを実現します。
GitHub概要
Brendonovich/prisma-client-rust
Type-safe database access for Rust
トピックス
スター履歴
ライブラリ
Prisma Client Rust
概要
Prisma Client Rustは「タイプセーフで自動生成されるRust用クエリビルダー」として開発された、PrismaエコシステムをRustで活用するためのORM(Object-Relational Mapping)crateです。一貫性のある理解しやすいAPIをPrismaの強力なデータベース技術の上に提供し、Diesel、SeaORM、SQLxのような既存のORMやツールの代替手段として機能。Rustアプリケーションにおいて、Prismaエコシステム全体を活用しながら、フロントエンド(Tauri等)からサーバーサイドまで一貫したタイプセーフなデータベースアクセスを実現します。
詳細
Prisma Client Rust 2025年版は、Brendonovich氏によってメンテナンスされる非公式プロダクトでありながら、PrismaのFOSS Fundの支援を受けて活発に開発が進められています。NodeJSからより高速で効率的なアプリケーション開発への移行を目指す開発者にとって、Prisma Client JSユーザーが慣れ親しんだツールと用語を活用し、Rustでのアプリケーション開発をよりアプローチしやすくする役割を果たします。初めてPrismaをフロントエンドアプリケーションで簡単に使用可能にし、Tauriを活用したデスクトップアプリケーション開発においてPrismaエコシステム全体を活用できる新しい可能性を提供します。
主な特徴
- 完全型安全: Rustの型システムを活用したコンパイル時安全性
- 自動生成クエリビルダー: Prismaスキーマから自動生成される型安全なAPI
- Prismaエコシステム統合: 既存のPrismaツールチェーンとの互換性
- SQLx統合: Pure Rustスタック(SeaORM + SQLx)の提供
- 非同期サポート: 非同期Rustプログラミングの全面サポート
- デスクトップアプリ対応: Tauriを活用したデスクトップアプリ開発に最適
メリット・デメリット
メリット
- Prismaエコシステムの成熟したツールと用語をRustで活用可能
- NodeJSからの移行において既存のPrismaスキルを活用できる
- DieselやSeaORMと比較してPrismaユーザーに親しみやすいAPI設計
- Tauriを使用したデスクトップアプリケーション開発での活用が容易
- Pure Rustスタック提供による一貫した開発体験
- 強力なPrismaマイグレーションシステムと開発ツールを継承
デメリット
- 非公式プロダクトのため公式サポートがなく、コミュニティサポートに依存
- Rust v1.62.0以上が必要で、比較的新しいRustバージョンが必須
- 特殊な設定が必要で、通常のRustプロジェクトとは異なるセットアップ
- prisma-client-rust-cliを別途作成する必要があり、追加設定が複雑
- Dieselに比べて成熟度が低く、エコシステムが限定的
- パフォーマンス面でDieselに劣る可能性がある
参考ページ
書き方の例
セットアップ
# Cargo.toml
[dependencies]
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11" }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
[build-dependencies]
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11" }
// schema.prisma
generator client {
provider = "cargo prisma"
output = "../src/prisma.rs"
}
generator db {
provider = "prisma-client-rust"
output = "../src/prisma.rs"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
基本的な使い方
// src/main.rs
use prisma_client_rust::*;
#[cfg(feature = "sqlite")]
#[path = "prisma.rs"]
mod prisma;
use prisma::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = PrismaClient::_builder().build().await?;
// データベース接続
client._db_push().await?;
println!("Database connected successfully!");
// 基本的なCRUD操作のデモ
basic_crud_example(&client).await?;
Ok(())
}
async fn basic_crud_example(client: &PrismaClient) -> Result<(), prisma_client_rust::QueryError> {
// ユーザー作成
let user = client
.user()
.create(
"[email protected]".to_string(),
vec![
user::name::set(Some("Alice Smith".to_string())),
],
)
.exec()
.await?;
println!("Created user: {:?}", user);
// ユーザー検索
let users = client
.user()
.find_many(vec![])
.exec()
.await?;
println!("All users: {:?}", users);
// 特定ユーザー取得
let alice = client
.user()
.find_unique(user::email::equals("[email protected]".to_string()))
.exec()
.await?
.unwrap();
println!("Found Alice: {:?}", alice);
// ユーザー更新
let updated_user = client
.user()
.update(
user::email::equals("[email protected]".to_string()),
vec![
user::name::set(Some("Alice Johnson".to_string())),
],
)
.exec()
.await?;
println!("Updated user: {:?}", updated_user);
// ユーザー削除
let deleted_user = client
.user()
.delete(user::email::equals("[email protected]".to_string()))
.exec()
.await?;
println!("Deleted user: {:?}", deleted_user);
Ok(())
}
クエリ実行
// 高度なクエリの例
use prisma_client_rust::*;
use crate::prisma::*;
pub struct UserService {
client: PrismaClient,
}
impl UserService {
pub fn new(client: PrismaClient) -> Self {
Self { client }
}
// 複数ユーザー作成
pub async fn create_users(&self, users_data: Vec<(String, Option<String>)>) -> Result<Vec<user::Data>, QueryError> {
let mut users = Vec::new();
for (email, name) in users_data {
let user = self.client
.user()
.create(
email,
vec![
user::name::set(name),
],
)
.exec()
.await?;
users.push(user);
}
Ok(users)
}
// 条件付き検索
pub async fn search_users(&self, name_filter: Option<String>) -> Result<Vec<user::Data>, QueryError> {
let mut where_clauses = vec![];
if let Some(name) = name_filter {
where_clauses.push(user::name::contains(name));
}
self.client
.user()
.find_many(where_clauses)
.order_by(user::created_at::order(SortOrder::Desc))
.exec()
.await
}
// ページネーション
pub async fn get_users_paginated(
&self,
page: i32,
page_size: i32
) -> Result<Vec<user::Data>, QueryError> {
let skip = (page - 1) * page_size;
self.client
.user()
.find_many(vec![])
.skip(skip)
.take(page_size)
.order_by(user::created_at::order(SortOrder::Desc))
.exec()
.await
}
// リレーション付きデータ取得
pub async fn get_users_with_posts(&self) -> Result<Vec<UserWithPosts>, QueryError> {
let users = self.client
.user()
.find_many(vec![])
.include(user::include!{
posts
})
.exec()
.await?;
Ok(users.into_iter().map(|user| UserWithPosts {
id: user.id,
email: user.email,
name: user.name,
created_at: user.created_at,
posts: user.posts.unwrap_or_default(),
}).collect())
}
// 集計クエリ
pub async fn get_user_statistics(&self) -> Result<UserStatistics, QueryError> {
let total_users = self.client
.user()
.count(vec![])
.exec()
.await?;
let users_with_posts = self.client
.user()
.count(vec![
user::posts::some(vec![])
])
.exec()
.await?;
Ok(UserStatistics {
total_users,
users_with_posts,
users_without_posts: total_users - users_with_posts,
})
}
}
// カスタム型定義
#[derive(Debug, Clone)]
pub struct UserWithPosts {
pub id: i32,
pub email: String,
pub name: Option<String>,
pub created_at: DateTime<FixedOffset>,
pub posts: Vec<post::Data>,
}
#[derive(Debug, Clone)]
pub struct UserStatistics {
pub total_users: i64,
pub users_with_posts: i64,
pub users_without_posts: i64,
}
データ操作
// 投稿管理サービス
pub struct PostService {
client: PrismaClient,
}
impl PostService {
pub fn new(client: PrismaClient) -> Self {
Self { client }
}
// 投稿作成(ユーザー確認付き)
pub async fn create_post(
&self,
author_email: String,
title: String,
content: Option<String>,
) -> Result<post::Data, Box<dyn std::error::Error>> {
// ユーザー存在確認
let author = self.client
.user()
.find_unique(user::email::equals(author_email.clone()))
.exec()
.await?
.ok_or(format!("User with email {} not found", author_email))?;
// 投稿作成
let post = self.client
.post()
.create(
title,
user::email::equals(author_email),
vec![
post::content::set(content),
],
)
.exec()
.await?;
Ok(post)
}
// 公開投稿の取得
pub async fn get_published_posts(&self) -> Result<Vec<PostWithAuthor>, QueryError> {
let posts = self.client
.post()
.find_many(vec![
post::published::equals(true)
])
.include(post::include!{
author
})
.order_by(post::created_at::order(SortOrder::Desc))
.exec()
.await?;
Ok(posts.into_iter().map(|post| PostWithAuthor {
id: post.id,
title: post.title,
content: post.content,
published: post.published,
created_at: post.created_at,
author: post.author.unwrap(),
}).collect())
}
// 投稿の公開
pub async fn publish_post(&self, post_id: i32) -> Result<post::Data, QueryError> {
self.client
.post()
.update(
post::id::equals(post_id),
vec![
post::published::set(true),
],
)
.exec()
.await
}
// 投稿検索
pub async fn search_posts(&self, query: String) -> Result<Vec<post::Data>, QueryError> {
self.client
.post()
.find_many(vec![
post::or(vec![
post::title::contains(query.clone()),
post::content::contains(query),
])
])
.order_by(post::created_at::order(SortOrder::Desc))
.exec()
.await
}
// バルク削除
pub async fn delete_unpublished_posts(&self) -> Result<i64, QueryError> {
self.client
.post()
.delete_many(vec![
post::published::equals(false)
])
.exec()
.await
}
}
#[derive(Debug, Clone)]
pub struct PostWithAuthor {
pub id: i32,
pub title: String,
pub content: Option<String>,
pub published: bool,
pub created_at: DateTime<FixedOffset>,
pub author: user::Data,
}
設定とカスタマイズ
// アプリケーション設定
use prisma_client_rust::*;
use std::env;
pub struct DatabaseConfig {
client: PrismaClient,
}
impl DatabaseConfig {
// データベース初期化
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let database_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "file:./dev.db".to_string());
let client = PrismaClient::_builder()
.with_url(database_url)
.build()
.await?;
// データベース接続テスト
client._db_push().await?;
println!("Database connected successfully");
Ok(Self { client })
}
pub fn client(&self) -> &PrismaClient {
&self.client
}
// データベース初期データ
pub async fn seed_database(&self) -> Result<(), QueryError> {
println!("Seeding database...");
// サンプルユーザー作成
let users = vec![
("[email protected]".to_string(), Some("Admin User".to_string())),
("[email protected]".to_string(), Some("Alice Smith".to_string())),
("[email protected]".to_string(), Some("Bob Johnson".to_string())),
];
for (email, name) in users {
let existing_user = self.client
.user()
.find_unique(user::email::equals(email.clone()))
.exec()
.await?;
if existing_user.is_none() {
self.client
.user()
.create(
email.clone(),
vec![
user::name::set(name),
],
)
.exec()
.await?;
println!("Created user: {}", email);
}
}
println!("Database seeding completed");
Ok(())
}
// データベースクリーンアップ
pub async fn cleanup_database(&self) -> Result<(), QueryError> {
println!("Cleaning up database...");
// 全投稿削除
let deleted_posts = self.client
.post()
.delete_many(vec![])
.exec()
.await?;
println!("Deleted {} posts", deleted_posts);
// 全ユーザー削除
let deleted_users = self.client
.user()
.delete_many(vec![])
.exec()
.await?;
println!("Deleted {} users", deleted_users);
Ok(())
}
}
// アプリケーションのメイン構造
pub struct App {
db: DatabaseConfig,
user_service: UserService,
post_service: PostService,
}
impl App {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let db = DatabaseConfig::new().await?;
let user_service = UserService::new(db.client().clone());
let post_service = PostService::new(db.client().clone());
Ok(Self {
db,
user_service,
post_service,
})
}
pub async fn initialize(&self) -> Result<(), Box<dyn std::error::Error>> {
self.db.seed_database().await?;
Ok(())
}
pub fn user_service(&self) -> &UserService {
&self.user_service
}
pub fn post_service(&self) -> &PostService {
&self.post_service
}
}
エラーハンドリング
// カスタムエラー型
use thiserror::Error;
use prisma_client_rust::QueryError;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] QueryError),
#[error("User not found: {email}")]
UserNotFound { email: String },
#[error("Post not found: {id}")]
PostNotFound { id: i32 },
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Authorization error: {message}")]
Authorization { message: String },
}
pub type AppResult<T> = Result<T, AppError>;
// エラーハンドリング付きサービス
pub struct SafeUserService {
client: PrismaClient,
}
impl SafeUserService {
pub fn new(client: PrismaClient) -> Self {
Self { client }
}
pub async fn create_user_safe(
&self,
email: String,
name: Option<String>,
) -> AppResult<user::Data> {
// バリデーション
if email.is_empty() || !email.contains('@') {
return Err(AppError::Validation {
message: "Invalid email format".to_string(),
});
}
// 重複チェック
let existing_user = self.client
.user()
.find_unique(user::email::equals(email.clone()))
.exec()
.await?;
if existing_user.is_some() {
return Err(AppError::Validation {
message: format!("User with email {} already exists", email),
});
}
// ユーザー作成
let user = self.client
.user()
.create(
email,
vec![
user::name::set(name),
],
)
.exec()
.await?;
Ok(user)
}
pub async fn get_user_by_email_safe(&self, email: String) -> AppResult<user::Data> {
let user = self.client
.user()
.find_unique(user::email::equals(email.clone()))
.exec()
.await?
.ok_or(AppError::UserNotFound { email })?;
Ok(user)
}
pub async fn update_user_safe(
&self,
email: String,
name: Option<String>,
) -> AppResult<user::Data> {
// ユーザー存在確認
self.get_user_by_email_safe(email.clone()).await?;
// 更新実行
let updated_user = self.client
.user()
.update(
user::email::equals(email),
vec![
user::name::set(name),
],
)
.exec()
.await?;
Ok(updated_user)
}
pub async fn delete_user_safe(&self, email: String) -> AppResult<user::Data> {
// ユーザー存在確認
self.get_user_by_email_safe(email.clone()).await?;
// 関連投稿の確認
let post_count = self.client
.post()
.count(vec![
post::author::is(vec![
user::email::equals(email.clone())
])
])
.exec()
.await?;
if post_count > 0 {
return Err(AppError::Validation {
message: format!("Cannot delete user with {} posts. Delete posts first.", post_count),
});
}
// 削除実行
let deleted_user = self.client
.user()
.delete(user::email::equals(email))
.exec()
.await?;
Ok(deleted_user)
}
}