prisma-client-rustを使うとprismaをRustで利用できる

これは[株式会社LabBase テックカレンダー Advent Calendar 2022](https://qiita.com/advent-calendar/2022/labbase)、16日目の記事です。

前日の記事は @denjiry さんのwasmによるReact上での単語サジェストの紹介でした。

最近のRustでのバックエンド開発でprismaを採用しているのですが、Typescriptによってシードデータの定義をしているため開発時にRustとTypescriptの両方を触る必要があり、頭の切り替えが煩わしく感じています。

Prismaのコア部分についてはRustで実装されているのでやろうと思えばRustだけでPrismaを利用できるはずだよな、と思いながら日々を過ごしていたのですが、今年になって[prisma-client-rust](https://github.com/Brendonovich/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](https://prisma.brendonovich.dev/getting-started/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のドキュメント通りに実装します。

```toml
# 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" }
```

```rust
// database/src/main.rs
fn main() {
    prisma_client_rust_cli::run();
}
```

`.cargo/cargo.toml`でエイリアスの設定をすることで、databaseプロジェクトのmain.rsのバイナリを`cargo prisma`というエイリアスで実行できるようになります(cargoでこういった設定ができることを初めて知りました)。

```toml
# database/.cargo/config.toml
[alias]
prisma = "run -p database --"
```

```bash
# 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"
}
```

```rust
// 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のコードだけを読み書きできる状態になりました。

```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` によって一度だけ実行するようにします。


```rust
// 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 さんの記事です。([こちら](https://qiita.com/sho-kanamaru/items/dce0d93e71e7815da8a8))