Prismaでモデルメソッドを実装する
こんにちはムーザルちゃんねるのzaruです。今回は、TypeScriptで書けるPrisma ORMでRailsのようなモデルメソッドとして実装する方法を紹介します。
Railsのようなモデルメソッドというのは、例えば User
テーブルがあったときに、Rubyでいうクラスメソッドとして User.signup()
という関数や、インスタンスメソッドとして User.findFirst().fullname
といった感じでドットのメソッドチェーンで関数やプロパティを呼び出せることを指します。
モデルメソッドの定義方法
モデルメソッドの定義方法は簡単で、PrismaClientに $extends
をつなげて定義するだけです。種類は4つあります。
-
model
: Railsでいうモデルクラスメソッド的なもの -
result
: Railsでいうモデルインスタンスメソッド的なものや、仮想フィールド的なもの -
client
: モデル関係なくPrismaクライアント自身を拡張する -
query
: 既存のクエリ関数を拡張する
この記事では最初の2つ model
と result
について取り上げます。
以下は、PrismaClientに signUp
と displayName
という2つの処理を拡張している例です。書き方はシンプルでテーブル名と定義名、それに対して処理を書くだけです。
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient().$extends({
model: {
user: {
// Userテーブル自身の処理を実装できる
async signUp(email: string, password: string) {
await prisma.user.create({ data: { email, password } });
},
},
},
result: {
user: {
// いわゆる仮想フィールド的なもの
displayName: {
needs: { email: true, name: true },
compute(user) {
return `${user.name} <${user.email}>`;
},
},
},
},
});
利用する書き方は以下の形です。
import { prisma } from "./prismaClient";
// クラスメソッド的に呼び出す
await prisma.user.signUp("email", "password");
// 特定のUserインスタンスメソッド的に呼び出す
const user = await prisma.user.findFirst();
if (user) {
console.log(user.displayName);
}
依存関係の定義
result
のインスタンスメソッド的なものの書き方に needs: { email: true, name: true }
というのがありますが、これは依存関係を表します。ここで定義したフィールドのみアクセス可能になります。
displayName: {
needs: { email: true, name: true },
compute(user) {
return user.id; // ❌ idはneedsで定義されてないのでアクセス不可
},
},
ただし、needs
自体を省略すると該当レコードのデータ全てにアクセス可能です。
displayName: {
compute(user) {
return user.name; // ✅ needs省略はどんな値にもアクセス可能
},
},
インスタンスメソッドで関数定義
上記の例だと displayName
というプロパティ定義でしたが、returnするのを値ではなく関数にすることで関数も定義可能です。
result: {
user: {
updateName: {
needs: { id: true },
compute({ id }) {
return async (name: string) => {
await prisma.user.update({ where: { id }, data: { name } });
};
},
},
},
},
computeは再利用される
Vueの算出プロパティのようなものをイメージすると近いかもしれません。compute
で計算されたものは基本再利用されるため、何回呼び出しても結果は同じになります。
const user = await prisma.user.findFirst();
if (user) {
console.log(user.displayName);
await user.updateName("new name"); // ここで名前を更新しても…
console.log(user.displayName); // この結果は↑最初のものと変わらない
}
モデル定義を別ファイルに分離する
上記のようにPrismaClientを愚直に拡張していくと、さまざまなテーブルの拡張が増え続けて見通しが悪くなります。そこで、モデル定義を別ファイルに分離しましょう。
適当に model/User.ts
といったファイルを作成します。そしてPrismaの拡張機能定義をする Prisma.defineExtension
を使って中身を移植します。先程まではPrismaClientを直接参照していましたが、この書き方の場合はコールバック関数でわたってきた client
オブジェクトを参照します。それ以外は書き方は同じです。
import { Prisma } from "@prisma/client";
export default Prisma.defineExtension((client) => {
return client.$extends({
name: "prisma-extension-user-model",
model: {
user: {
async signUp(email: string, password: string) {
await client.user.create({ data: { email, password } });
},
},
},
result: {
user: {
displayName: {
needs: { email: true, name: true },
compute(user) {
return `${user.name} <${user.email}>`;
},
},
},
},
});
});
あとは定義した拡張機能をPrismaClient側で呼び出すだけです。
import { PrismaClient } from "@prisma/client";
import userModel from "./model/User";
export const prisma = new PrismaClient().$extends(userModel);
これでかなり見た目がスッキリしましたね。
以下の動画の最後でも質問していますが、こういった使い方をしている人を見たことがないです。もし使っている人がいたらコメントで教えて下さい。もしくは使おうと思ったけど止めた人も、止めた理由があればコメントで教えて下さい。
Discussion