4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PrismaORMで複数のデータベーススキーマを動的に管理する

Posted at

なにこれ

とあるチーム開発でNext.jsを採用し、ORMとして初めてPrismaを触ってみました。そこで、RDB特有のデータベーススキーマの構造を利用して、アカウントごとに同じテーブル構造のスキーマを作成し、テーブル群をpublicスキーマと分割して扱うという設計に走りました。
要するにアプリケーションの方でアカウントが登録されると、スキーマのテンプレートをもとにアカウントidと同じ名前のデータベーススキーマが作成されるような流れ。

スクリーンショット 2023-12-13 3.48.23.png

しかし、PrismaORMは基本的にprisma/schema.prismaに定義されているmodel群に対してのみ扱えるClientインスタンスができる仕組みになっています。特に指定がない場合、定義されたmodelはすべてpublicスキーマに作成されます。

client.js
import { PrismaClient } from '@prisma/client'

// 普通にimportすると、schema.prismaに定義したmodelだけ扱えるclientになる
const prisma = new PrismaClient()

// prismaインスタンスに任意の.tablename変数でtableにアクセスする
await prisma.tablename.create({
    data: {
        ...
    },
})

一応、事前に指定されたデータベーススキーマにmodelを定義したい場合、multiple database schemasという手法で定義することはできます。

schema.prisma
generator client {
    provider        = "prisma-client-js"
+   previewFeatures = ["multiSchema"]
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
+   schemas  = ["base", "transactional"] // あらかじめ作っておくことを想定
}

しかしこれはテーブルやスキーマが新しく作成される場合では不適です。schema.prismaを動的に変更して更新する必要がありますし、できたとしても作成されるスキーマにテーブルが5個もあったら、それだけでmodelが5つも追記されることになるので、schema.prismaが肥大化していきます。やはりclientごとに.prismaファイルは分割するべきでしょう。

まとめると、以下の問題を解決したい

  • プログラムでデータベーススキーマを作成したい
  • アクセスするデータベーススキーマを動的に切り替えたい
  • 複数のclientを扱いたい

複数のclientを扱う

これを前提としないと残り2つがうまく説明できないので、こちらから見ていきます。調べてみると、優秀な著者様がいらっしゃったので、紹介と引用をさせていただきます。

こちらはどちらもデータベーススキーマではなく、異なるデータベースごとにClientを作成する手順を紹介しています。しかし、schema.prismaをデータベースごとに作ってそれぞれにインスタンスを作る手法はとても参考になりました。本当にありがとうございます。(届かぬ声)

.prismaの定義

例えば、userAとuserBというスキーマを作り、それぞれにClientインスタンスを作成したい場合、.prismaファイルの先頭を次のように定義します。

userA.prisma
generator client {
  provider = "prisma-client-js"
+  output   = "../generated/userA"
}

datasource db {
  provider = "postgresql"
+  url      = "postgres://...?schema=userA"
}
userB.prisma
generator client {
  provider = "prisma-client-js"
+  output   = "../generated/userB"
}

datasource db {
  provider = "postgresql"
+  url      = "postgres://...?schema=userB"
}

ポイントとなるのは、outputパラメータと、urlの末尾です。

  • output:Clientインスタンスのimport先となるディレクトリーを設定できます。
  • url:databaseのurlの末尾にクエリパラメータとして?schema=を入れることで、指定したデータベーススキーマ内を指定します。

prismaコマンド

.prismaを書き終えたあと、次のコマンドを実行します。

terminal
$ npx prisma migrate dev --name userA --schema prisma/userA.prisma
$ npx prisma generate --schema prisma/userA.prisma

$ npx prisma migrate dev --name userB --schema prisma/userB.prisma
$ npx prisma generate --schema prisma/userB.prisma
  • migrate devはデータベースに.prismaで定義したmodelを適応させます。
  • generateは、.prismaで定義したモデルのインスタンスをoutputのディレクトリに作成します。

ここでえらいのは、コマンドに--schemaパラメータを設定できる点です。実はこれ、なくても動くのですが、デフォルトはprisma/schema.prismaになっています。つまりこのパラメータを使うことで、任意のファイル名の.prismagenerateできるわけです。大感謝。

Clientのexport

migrationsディレクトリなどが作成されますが、大体見るとこんな感じになっているのではないでしょうか。

terminal
$ tree prisma
prisma
├── generated
│   ├── userA
│   └── userB
├── userA.prisma
└── userB.prisma

ここで、clientディレクトリを作って、userA.jsuserB.jsを作ります。

terminal
$ tree prisma
prisma
├── client
│   ├── userA.js
│   └── userB.js
├── generated
│   ├── userA
│   └── userB
├── userA.prisma
└── userB.prisma

この.jsで、generatedからそれぞれのデータベーススキーマに対してclientインスタンスを作成します。

userA.js
import {PrismaClient as UserA} from '../generated/userA';

const user_a = new UserA({
    datasources:{
        db:{
            url: "postgres://...?schema=userA"
            }
    }});

export default user_a;
userB.js
import {PrismaClient as UserB} from '../generated/userA';

const user_b = new UserB({
    datasources:{
        db:{
            url: "postgres://...?schema=userB"
            }
    }});

export default user_b;

これらひとつのjsにまとめたかったのですが、ひとつの.jsにある複数のexportを動的に切り替えることができなかったので、.jsのパスによって動的にimport先を切り替えるようにします。

データベーススキーマを動的に切り替える

requireでimportします。promiseが返されるので、非同期で解きます。.defaultexport defaultされているオブジェクトを参照しています。この場合、importClient()関数の引数によって動的にimport先を切り替えることが可能になっています。

crud.js
async function importClient(user){
    const client = require(`prisma/clients/${user}.js`);
    return client;
};

const userSchema = await importClient("userA");
const tableInst = userSchema.default.tablename;

await tableInst.create({
    data: {
        ...
    },
})

これで複数のclientを扱う流れを理解できました!

プログラムでデータベーススキーマを作成する

実はここが本題でした。 今回の要件では、アカウントが作られるたびに同じテーブルがセットされている新しいスキーマが作成される設計になっています。ここまで期待をさせていてとても申し訳ない回答になるかと思いますが、ゴリ押しでやります。というのも、上記のclientを作成する流れをそのままプログラムでやるだけです。

.prismaを生成する

まずスキーマ構造のテンプレートとなるtemplate.prismaを作っておきます。(.prismaの定義

template.prisma
generator client {
  provider = "prisma-client-js"
  output   = "../generated/ACCOUNTID"
}

datasource db {
  provider = "postgresql"
  url      = "postgres://...?schema=ACCOUNTID"
}

model Table1 {
...

これをFsを使って書き換えて複製します。

create_schema.js
import Fs from 'fs';

const createSchema = (accountId) => { 
    Fs.readFile("./prisma/template.prisma", 'utf8', (err, data) => {
        // ACCOUNTIDをaccountId引数に書き換える
        const modifiedData = data.replace(/ACCOUNTID/g, accountId);
        
        // 変更した内容で.prismaを複製する
        Fs.writeFile(`./prisma/${accountId}.prisma`, modifiedData, 'utf8', (err) => {
            if (err) {
                console.error(err);
            } else {
                console.log(`${accountId}.prisma generated`);
            }
        });
    });
};

prismaコマンドを実行する

execを使ってコマンドを実行させます。(prismaコマンド

create_schema.js
import { exec } from 'child_process';

...
// データベースを更新
exec(`npx prisma migrate dev --name ${accountId} --schema prisma/${accountId}.prisma`, 
        (err, stdout, stderr) => {
    if (err) {
        console.error(err)
    }
        console.log("migrate done")
    }
);

// clientを作成する
exec(`npx prisma generate --schema prisma/${accountId}.prisma`, 
        (err, stdout, stderr) => {
    if (err) {
        console.error(err)
    }
        console.log("client generate done")
    }
);

Clientのexportを定義する

.jsファイルを作って、そこにexport元となるclientを定義します。これもFsを使って.jsファイルを作ります。(Clientのexport

create_schema.js
...
// clientを作成する
const clientcontent =  `import {PrismaClient as ${accountId}} from '../generated/${accountId}';\n
                        const ${accountId} = new ${accountId}({datasources:{db:{url:"postgres://...?schema="+'${accountId}'}}});\n
                        export default ${accountId};\n`;

Fs.writeFile(`./prisma/client/${accountId}.js`, clientcontent, 'utf8', (err) => {
    if (err) {
        console.error(err);
    } else {
        console.log('clients created');
    }
});

これで以下の要件を実現することができました。

  • 動的なデータベーススキーマの作成
  • 動的なclientインスタンスをの定義
  • 動的なclientインスタンスの切り替え
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?