以前、prisma-clientでGraphQLに入門しました。

せっかくRustを触りだしたので、prismaでRustクライアントを出力できるprisma-client-rustを使用してRESTアプリケーションを構築してみたいと思います。

GitHub - Brendonovich/prisma-client-rust: Type-safe database access for Rust

Cargoパッケージの初期化

まずは元になるパッケージを作成します。

% cargo new prisma-for-rest
     Created binary (application) `prisma-for-rest` package

せっかくこの前学んだので、このパッケージに移動して安定版の最新バージョンを参照するrust-toolchain.tomlを作っておきます。

[toolchain]
channel = "stable-2023-07-12"

続けてデータベース用のパッケージを作成します。

cargo new database

データベース用のパッケージに移動し、prisma-client-rustを依存関係に追加します。

[dependencies]
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.8" }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.8" }

database/src/main.rsに、prisma-client-rust用のCLIを操作するための実装を追記します。

fn main() {
    prisma_client_rust_cli::run();
}

prisma-client-rust-cliの呼び出しを簡素化するため、.cargo/config.tomlというファイルを作成して以下のような設定を追記します。

[alias]
prisma = "run --"

これにより、cargo prismaでprisma-client-rust-cliの呼び出しが可能になりました。

% cargo prisma --help 
    Finished dev [unoptimized + debuginfo] target(s) in 0.49s
     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
        validate   Validate your Prisma schema
          format   Format your Prisma 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

  Validate your Prisma schema
  $ prisma validate

  Format your Prisma schema
  $ prisma format

prismaスキーマの初期化

Prisma CLIが使えるようになったので、CLIからPrismaの初期化を行います。

データベースはガッツリしたのを使いたくないので、SQLiteを使用しました。

% cargo prisma init --datasource-provider sqlite
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/database init --datasource-provider sqlite`

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

初期化されると、prisma/schema.prisma.envが出来上がっています。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"

スキーマには、prismaのチュートリアルでおなじみのユーザーとポストのモデルを使っていきます。

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

この内容で一旦マイグレーションします。

%cargo prisma migrate dev       
    Finished dev [unoptimized + debuginfo] target(s) in 1.06s
     Running `target/debug/database migrate dev`
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

SQLite database dev.db created at file:./dev.db

✔ Enter a name for the new migration: … init
Applying migration `20230805123330_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20230805123330_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)
✔ Created ./package.json

added 2 packages, and audited 3 packages in 9s

found 0 vulnerabilities

added 2 packages, and audited 5 packages in 7s

found 0 vulnerabilities

✔ Installed the @prisma/client and prisma packages in your project
Error: Generator at /home/username/Project/study/prisma-for-rest/database/node_modules/@prisma/client/generator-build/index.js could not start:

マイグレーションはできましたが、prisma generate時になんかエラーが出ているようです。

node_modulesのインストールが行われているので、jsのPrismaクライアントが作成されてしまっているようです。

スキーマのgeneratorセクションを修正します。

generator client {
    provider      = "cargo prisma"
    output        = "../src/prisma.rs"
}

再度prisma generateを実行します。

% cargo prisma generate
    Finished dev [unoptimized + debuginfo] target(s) in 1.24s
     Running `target/debug/database generate`
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client Rust to ./prisma/src/prisma.rs in 643ms

成功しましたね。

PrismaのRustクライアントでデータ操作する

先程のprisma generateで、database/src/prisma.rsが作成されているはずです。

これを使用することで、SQLiteのデータベース操作を行うことができるようになります。

とりあえず、prisma.rsを使用するにあたって必要な依存ライブラリをdatabaseに追加します。

[dependencies]
# ...中略...
random-string = "1.0.0" # ランダムな値のレコード生成に使用します
serde = "1.0.181" # prisma.rsで使用するライブラリです
tokio = "1.29.1" # prisma-clientの実行に利用します

prisma.rsのクライアントを実行するために、lib.rsにprismaモジュールを定義します。

#[allow(warnings, unused)]
pub mod prisma;

更にdatabase/src/bin/prisma_controller.rsを作成し、prismaモジュールから操作を実行してみましょう。

use random_string::generate;
use database::prisma::PrismaClient;

#[tokio::main]
async fn main() {
    let client: PrismaClient = PrismaClient::_builder().build().await.unwrap();

    // メールアドレスの作成(ランダム文字列を使用)
    let email = format!("{}@test.com", generate(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")).to_string();
    // ユーザーの作成
    let user = client.user().create(email.clone(), vec![]).exec().await.unwrap();

    // 先程作成したユーザーに紐づく投稿を作成
    client
        .post()
        .create(
            "what up".to_string(),
            database::prisma::user::UniqueWhereParam::IdEquals(user.id.clone()),
            vec![],
        )
        .exec()
        .await.unwrap();
}

上記のように実装しました。

これを実行してみましょう。

%cargo run --bin prisma_controller   
   Compiling fastrand v1.9.0
   Compiling random-string v1.0.0
   Compiling database v0.1.0 (/home/username/Project/study/prisma-for-rest/database)
    Finished dev [unoptimized + debuginfo] target(s) in 30.09s
     Running `/home/username/Project/study/prisma-for-rest/target/debug/prisma_controller`

実行できたようです。

sqliteで確認してみます。

% sqlite3 prisma/dev.db 
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.

sqlite> select * from User join Post on User.id = Post.authorId;
1|Yzru16Cv7c@test.com||1|what up||0|1

作成されていますね!

しっかり生成したprismaクライアントが使えているようです。

actix-webから操作してみる

では本題のRESTアプリケーションを実装してみましょう。

といっても、サンプル実装があるのでこれをベースに実装します。

prisma-client-rust/examples/actix/src/main.rs at main · Brendonovich/prisma-client-rust · GitHub

prisma-for-restに戻って、新たにREST API用のパッケージを作成します。

cargo new rest-api

ワークスペースにrest-apiパッケージを追加します。

[workspace]
members = [
    "database",
    "rest-api", # これを追加
]

rest-apiパッケージに移動して、prismaクライアントとactixの依存関係を追加していきます。

[dependencies]
actix-web = "4.3.1"
database = { path = "../database" }

とりあえずactixの起動とユーザーの取得ができるようにmain.rsを実装します。

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use database::prisma::PrismaClient;

#[get("/users")]
async fn get_users(client: web::Data<PrismaClient>) -> impl Responder {
    let users = client.user().find_many(vec![]).exec().await.unwrap();

    HttpResponse::Ok().json(users)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    
    let client = PrismaClient::_builder().build().await.unwrap();
    let data = web::Data::new(client);

    HttpServer::new(move || {
        App::new()
            .app_data(data.clone())
            .service(get_users)
    })
    .bind(("127.0.0.1", 3001))?
    .run()
    .await
}

実行すると以下のようなエラーが出ました。

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', /home/username/.cargo/git/checkouts/prisma-client-rust-fa967aa5ad0ec391/51f0473/src/client.rs:167:18

一旦実装をコメントアウトしてtokioを依存関係に追加し、先ほどdatabaseパッケージで実装したクライアント実装を持ってきて再実行してみました。

use database::prisma::PrismaClient;

#[tokio::main]
async fn main() {
    let client: PrismaClient = PrismaClient::_builder().build().await.unwrap();

    let email = "test@test.com".to_string();
    // ユーザーの作成
    let user = client.user().create(email.clone(), vec![]).exec().await.unwrap();

    // 先程作成したユーザーに紐づく投稿を作成
    client
        .post()
        .create(
            "what up".to_string(),
            database::prisma::user::UniqueWhereParam::IdEquals(user.id.clone()),
            vec![],
        )
        .exec()
        .await.unwrap();
}

しかし同じエラーになってしまう。。

どうやら、実行ディレクトリ内に.envがないと実行できないようです。

Allow to build with env("DATABASE_URL") in prisma.schema · Issue #299 · Brendonovich/prisma-client-rust · GitHub

rest-api/.envにデータベースのURLを設定しておきます。

DATABASE_URL="file:../database/prisma/dev.db"

この状態でactixの実装に戻して、再度実行してみます。

% cargo run
   Compiling rest-api v0.1.0 (/home/username/Project/study/prisma-for-rest/rest-api)
    Finished dev [unoptimized + debuginfo] target(s) in 27.17s
     Running `/home/username/Project/study/prisma-for-rest/target/debug/rest-api`

起動できました。

curlでユーザー一覧を取得してみます。

% curl http://localhost:3001/users
[{"id":1,"email":"Yzru16Cv7c@test.com","name":null,"posts":null},{"id":2,"email":"test@test.com","name":null,"posts":null}]

取得できていますね!

補足:prisma.rsはコードリポジトリ上で管理すべきではない

The generated client must not be checked into source control. It cannot be transferred between devices or operating systems. You will need to re-generate it wherever you build your project. If using git, add it to your .gitignore file.

和訳:生成されたクライアントはソース管理にチェックインしないでください。デバイス間やオペレーティング システム間で転送することはできません。プロジェクトを構築する場合はどこでも再生成する必要があります。git を使用している場合は、それを .gitignore ファイルに追加します。

Setup – Prisma Client Rust

なので、今回Prismaが生成したdatabase/src/prisma.rsは、毎回cargo prisma generateして作成してあげる必要があるファイルとのことです。

プロジェクトで使用する場合はかならず.gitignoreに追加してあげるようにしましょう。

まとめ

とりあえずactixとprisma-client-rustで実装ができました。

ただ、データベースURLの管理についてはdotenv頼りではなくちゃんと管理してあげる必要がありますね。

ながくなってきたのでこのへんで終わりにします。