これは株式会社LabBase テックカレンダー Advent Calendar 2022、16日目の記事です。
前日の記事は @denjiry さんのwasmによるReact上での単語サジェストの紹介でした。
最近のRustでのバックエンド開発でprismaを採用しているのですが、Typescriptによってシードデータの定義をしているため開発時にRustとTypescriptの両方を触る必要があり、頭の切り替えが煩わしく感じています。
Prismaのコア部分についてはRustで実装されているのでやろうと思えばRustだけでPrismaを利用できるはずだよな、と思いながら日々を過ごしていたのですが、今年になってprisma-client-rustが有志によって作成されたので、自分の知識のアップデートも兼ねてprisma-client-rust
を使ってRustだけでPrismaを利用するやり方について、紹介しようと思います。
プロジェクトの構成(prisma-client-rust導入前)
databaseディレクトリ配下でprismaによるテーブル定義やテストデータの管理、webappディレクトリ配下でRustのバックエンドコードの管理を行っています。シードデータはTypescriptで実装されており、database配下にprisma-client-rust
を導入していくことで、Typescriptの依存性を減らしていきます。
.
├── database // prismaによるテーブル定義の管理
│ ├── migrations
│ ├── package.json
│ ├── schema.prisma
│ ├── src // シードデータの実装によるテストデータ作成(Typescript)
│ ├── tsconfig.json
│ └── yarn.lock
└── webapp // Rustのバックエンドプロジェクト
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── application
├── base
├── build.rs
├── domain
├── infrastructure
├── src
└── tests
prisma-client-rustの導入
installationに従って導入を行っていきます。prisma-client-rust
とprisma-client-rust-cli
の2種類のクレートが存在していますが、前者はORマッパーとして使う際に必要なクレートで後者はprisma
コマンドの実行に必要なクレートのようです。
いくつかの導入パターンについて紹介がありますが、
- databaseディレクトリ内をcargoプロジェクトに変更して
- main.rsで、prismaコマンド(と同等の振る舞いをする)バイナリを生成する
- lib.rsで、テストデータを投入するヘルパーを作成する
上記の方針で置き換えてみます。
main.rsでprismaコマンド(と同等の振る舞いをする)バイナリを生成する
cargo init
でcargoプロジェクト配下をcargoプロジェクトに変更して、main.rsをprisma-client-rustのドキュメント通りに実装します。
# database/Cargo.toml
[package]
name = "database"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.57"
serde = { version = "1.0", features = ["derive"] }
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.3", default-features = false, features = ["mysql", "migrations"] }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.3" }
// database/src/main.rs
fn main() {
prisma_client_rust_cli::run();
}
.cargo/cargo.toml
でエイリアスの設定をすることで、databaseプロジェクトのmain.rsのバイナリをcargo prisma
というエイリアスで実行できるようになります(cargoでこういった設定ができることを初めて知りました)。
# database/.cargo/config.toml
[alias]
prisma = "run -p database --"
# database配下では、cargo prismaのコマンドが利用可能になる
cargo prisma --help
Finished dev [unoptimized + debuginfo] target(s) in 0.57s
Running `target/debug/database --help`
◭ Prisma is a modern DB toolkit to query, migrate and model your database (https://prisma.io)
Usage
$ prisma [command]
Commands
init Set up Prisma for your app
generate Generate artifacts (e.g. Prisma Client)
db Manage your database schema and lifecycle
migrate Migrate your database
studio Browse your data with Prisma Studio
format Format your schema
Flags
--preview-feature Run Preview Prisma commands
Examples
Set up a new Prisma project
$ prisma init
Generate artifacts (e.g. Prisma Client)
$ prisma generate
Browse your data
$ prisma studio
Create migrations from your Prisma schema, apply them to the database, generate artifacts (e.g. Prisma Client)
$ prisma migrate dev
Pull the schema from an existing database, updating the Prisma schema
$ prisma db pull
Push the Prisma schema state to the database
$ prisma db push
lib.rsで、テストデータを投入するヘルパーを作成する
ORマッパーのコード生成
databaseプロジェクト内でprisma
コマンドが実行できるようになったので、ORマッパーのコードを生成します。schema.prismaのジェネレーターの設定でproviderにcargo prisma
を指定すると、output
で指定されたパスにRustのORマッパーのコードが生成されるようです。
// database/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "cargo prisma"
output = "src/db.rs"
}
// database/src/db.rsに自動生成されたコード
// Code generated by Prisma Client Rust. DO NOT EDIT
#![allow(warnings, unused)]
pub static DATAMODEL_STR: &'static str =
include_str!("/path/to/project/database/schema.prisma");
static DATABASE_STR: &'static str = "mysql";
pub async fn new_client() -> Result<PrismaClient, ::prisma_client_rust::NewClientError> {
PrismaClient::_builder().build().await
}
pub async fn new_client_with_url(
url: &str,
) -> Result<PrismaClient, ::prisma_client_rust::NewClientError> {
PrismaClient::_builder()
.with_url(url.to_string())
.build()
.await
}
// 自動生成されたコードが続く
prisma db seedに相当するコードを書く
生成されたコードを元にシードデータの投入処理をRustで実装します。Typescriptで実装していたものがRustで実装できるので、prisma周りについてもRustのコードだけを読み書きできる状態になりました。
// database/src/lib.rs
mod db;
mod seed;
use anyhow::Result;
use seed::service::create_service_seed_data;
use crate::db::new_client_with_url;
pub async fn seed(url: &str) -> Result<()> {
let client = new_client_with_url(url).await?;
// 各シードデータのセットアップ処理を呼び出しする
create_service_seed_data(&client).await?;
Ok(())
}
// 適当なシードデータを投入する処理です(services, service_localesというテーブルに投入するデータです)
// database/src/seed/service.rs
use anyhow::Result;
use crate::db::{service, LanguageCode, PrismaClient};
struct ServiceLocale {
language_code: LanguageCode,
name: String,
description: String,
url: String,
}
struct Service {
id: i32,
service_id: String,
display_order: i32,
enable: bool,
locales: Vec<ServiceLocale>,
}
// サービスデータを追加する
pub async fn create_service_seed_data(prisma_client: &PrismaClient) -> Result<()> {
// シードデータの定義
let services = vec![Service {
id: 6,
service_id: String::from("test"),
display_order: 6,
enable: true,
locales: vec![ServiceLocale {
language_code: LanguageCode::Ja,
name: String::from("テスト"),
description: String::from("テストサービスの説明"),
url: String::from("http://example.com"),
}],
}];
for service in services {
prisma_client
.service()
.create(
service.service_id,
service.display_order,
// 必須項目以外の値をSetParamで変更できる
vec![
service::SetParam::SetId(service.id),
service::SetParam::SetEnable(service.enable),
],
)
.exec()
.await?;
prisma_client
.service_locale()
.create_many(
service
.locales
.iter()
.map(|locale| {
(
service.id,
locale.language_code,
locale.name.clone(),
locale.description.clone(),
locale.url.clone(),
vec![],
)
})
.collect(),
)
.exec()
.await?;
}
Ok(())
}
※現時点ではprisma db seed
を実行しても、package.jsonで指定したTypescriptのコードが実行されるようなので、lib.rsでテストデータを投入するヘルパーを作成し、テスト実行時に呼び出すようにします(prisma-client-rustが開発されていくとCargo.tomlに設定できるようになるかもしれないですね)。
バックエンドのテストで利用する
database側で追加した処理をバックエンド側で利用します。
prismaによるmigrationの実行とテストデータの投入
マイグレーションについてはcargo prisma
コマンドをstd::process::Command
で実行することで実行し、シードデータの投入についてはdatabase/src/lib.rs
で作成したシード投入関数を呼び出すことで実行します。1度だけ実行したいので、tokio::sync::OnceCell
によって一度だけ実行するようにします。
// webapp/infrastructure/src/repository.rs
// テスト用ユーティリティの定義
#[cfg(test)]
pub mod test_util {
use std::{process::Command, sync::Once};
static ONCE: OnceCell<()> = OnceCell::const_new();
// 初期化処理(OnceCellで1度だけ実行する)
pub async fn init_db() {
ONCE.get_or_init(init_seed_data).await;
}
// 初期化処理の実態
async fn init_seed_data() {
// 色々初期化を行う
dotenv().ok();
setup_logger().unwrap();
// prismaのマイグレーションの実行
Command::new("cargo")
.args([
"prisma",
"migrate",
"reset",
"--skip-seed",
"-f",
"--skip-generate",
"--schema",
"../database/schema.prisma",
])
.output()
.expect("failed to execute migration");
// シードデータの投入
let url = String::from("mysql://id:pw@localhost:3306/schema");
// database/src/lib.rsで定義したシードデータ投入関数を呼び出す
seed(&url).await.unwrap();
}
}
// 利用側
#[cfg(test)]
mod test {
#[tokio::test]
async fn test_hoge() {
// dbのセットアップ処理を呼び出す
init_db().await;
// テストコードを実装する
}
}
終わりに
以上、prisma-client-rust
を使ってTypescriptを使わずにprismaをRustで利用する紹介でした。
次回の記事は @sho-kanamaru さんの記事です。(こちら)