💽

Prismaでモデルメソッドを実装する

2024/10/10に公開

こんにちはムーザルちゃんねるのzaruです。今回は、TypeScriptで書けるPrisma ORMでRailsのようなモデルメソッドとして実装する方法を紹介します。

Railsのようなモデルメソッドというのは、例えば User テーブルがあったときに、Rubyでいうクラスメソッドとして User.signup() という関数や、インスタンスメソッドとして User.findFirst().fullname といった感じでドットのメソッドチェーンで関数やプロパティを呼び出せることを指します。

モデルメソッドの定義方法

モデルメソッドの定義方法は簡単で、PrismaClientに $extends をつなげて定義するだけです。種類は4つあります。

  • model : Railsでいうモデルクラスメソッド的なもの
  • result : Railsでいうモデルインスタンスメソッド的なものや、仮想フィールド的なもの
  • client : モデル関係なくPrismaクライアント自身を拡張する
  • query : 既存のクエリ関数を拡張する

この記事では最初の2つ modelresult について取り上げます。

以下は、PrismaClientに signUpdisplayName という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);

これでかなり見た目がスッキリしましたね。


以下の動画の最後でも質問していますが、こういった使い方をしている人を見たことがないです。もし使っている人がいたらコメントで教えて下さい。もしくは使おうと思ったけど止めた人も、止めた理由があればコメントで教えて下さい。

https://youtu.be/oHU89_c02cs

ムーザルちゃんねる

Discussion