なにこれ
とあるチーム開発でNext.jsを採用し、ORMとして初めてPrismaを触ってみました。そこで、RDB特有のデータベーススキーマの構造を利用して、アカウントごとに同じテーブル構造のスキーマを作成し、テーブル群をpublicスキーマと分割して扱うという設計に走りました。
要するにアプリケーションの方でアカウントが登録されると、スキーマのテンプレートをもとにアカウントidと同じ名前のデータベーススキーマが作成されるような流れ。
しかし、PrismaORMは基本的にprisma/schema.prisma
に定義されているmodel群に対してのみ扱えるClientインスタンスができる仕組みになっています。特に指定がない場合、定義されたmodelはすべてpublicスキーマに作成されます。
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という手法で定義することはできます。
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
ファイルの先頭を次のように定義します。
generator client {
provider = "prisma-client-js"
+ output = "../generated/userA"
}
datasource db {
provider = "postgresql"
+ url = "postgres://...?schema=userA"
}
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
を書き終えたあと、次のコマンドを実行します。
$ 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
になっています。つまりこのパラメータを使うことで、任意のファイル名の.prisma
をgenerate
できるわけです。大感謝。
Clientのexport
migrations
ディレクトリなどが作成されますが、大体見るとこんな感じになっているのではないでしょうか。
$ tree prisma
prisma
├── generated
│ ├── userA
│ └── userB
├── userA.prisma
└── userB.prisma
ここで、client
ディレクトリを作って、userA.js
、userB.js
を作ります。
$ tree prisma
prisma
├── client
│ ├── userA.js
│ └── userB.js
├── generated
│ ├── userA
│ └── userB
├── userA.prisma
└── userB.prisma
この.js
で、generated
からそれぞれのデータベーススキーマに対してclientインスタンスを作成します。
import {PrismaClient as UserA} from '../generated/userA';
const user_a = new UserA({
datasources:{
db:{
url: "postgres://...?schema=userA"
}
}});
export default user_a;
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が返されるので、非同期で解きます。.default
でexport default
されているオブジェクトを参照しています。この場合、importClient()
関数の引数によって動的にimport先を切り替えることが可能になっています。
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の定義)
generator client {
provider = "prisma-client-js"
output = "../generated/ACCOUNTID"
}
datasource db {
provider = "postgresql"
url = "postgres://...?schema=ACCOUNTID"
}
model Table1 {
...
これをFs
を使って書き換えて複製します。
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コマンド)
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)
...
// 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インスタンスの切り替え