Prismaが返すオブジェクトに機能をつけたい!

2024/05/21に公開

Prismaが返すオブジェクト

RubyとRailsばかり書いてきたぼくも、ここ最近はTypeScriptばかり書いています。
今開発しているサービスはNestJSを使っているのですが、DBのデータを取得するのにPrismaを使っています。
Prismaでは以下のようにしてデータを取得できますよね。

const user = await prisma.user.findUnique({
  where: { id: userId }
});

console.dir(user) 
{
  id: 1,
  first_name: "つかさ",
  last_name: "おおいし",
  age: 49,
}

ここで返ってくるオブジェクトはTypeScriptのインターフェースに基づいた型安全なオブジェクトです。このオブジェクトはデータのみを持ち、メソッドは持っていません。

そのモデルが普遍的にもつ機能なら、このオブジェクトにメソッドが生えててほしいなー。
このあたりのやり方をいろいろ調べてみました。

classを使う

RailsならここはActiveRecordオブジェクトになるので、機能を生やすのは簡単です。

class User < ActiveRecord::Base
  def full_name
    [first_name, last_name].join(" ")
  end
end

同じノリでモデルクラスを定義してメソッドを生やしてみます。

export class User {
  readonly id: number;
  readonly firstName: string;
  readonly lastName: string;
  readonly age: number;

  constructor(id: number, firstName: string, lastName: string, age: number) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age
  }

  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

repositoryでデータを取得した後はこのクラスにマッピングします。

async findById(id: number): Promise<User | null> {
  const user = await this.prisma.user.findUnique({
    where: { id }
  });

  if (user === null) return null;

  return new User(
    id: user.id,
    firstName: user.firstName,
    lastName: user.lastName,
    age: user.age
  );
}

ここまで見てどう思いましたか?

すごく面倒くさい!!!!

Prismaがスキーマで定義したとおりのオブジェクトを返してくれているのに、わざわざ人間が同じようなコードを書いているのです。得たいのは fullName 関数だけなのに...。

コード量が増えれば増えるほど得られるものがあります。ユーザに提供できる価値? そうかもしれません。確実に言えるのは、コード量が増えるほど不具合が入り込む可能性も増えるということです。
なんかこのあたりしゅっとできるnpmとか作ろうかな。

まあ、再定義チックなコードはおいておいて、そのモデルが提供する機能をそこに素直に書けるのはいいところ。
あと、repository内でclassへのマッピングを完全に行うなら、repository層より上にはPrismaの実装がにじみ出ないのでそこもよさそう。気が向いたらPrismaから別の何かに変えることもできますね(あまりやらないとは思うけど)。

joinしたデータが存在する場合、そのjoinのパターンごとにclassを定義する必要がありそうです。
あとselect指定に対応するならデータをoptionalにするか、そもそもselect指定はしないという方針にしないといけないですね。

Prismaが返すオブジェクトにメソッドを生やす

オブジェクトのデータ自体はPrismaがいい感じに扱ってくれるので、メソッドだけを書く方法を試してみます。
というか、たぶんみんなが最初に思いつく方法っぽい気はする。

import { User as PrismaUser } from '@prisma/client'
interface User extends PrismaUser {
  fullName (): string
}

const fullName = function(this: PrismaUser): string {
  return `${this.firstName} ${this.lastName}`;
}

export function toUser(user: PrismaUser): User {
  return {
    ...user,
    fullName,
  };
}
async findById(id: number): Promise<User | null> {
  const user = await this.prisma.user.findUnique({
    where: { id }
  });

  if (user === null) return null;

  return toUser(user);
}

再定義チックなコードを書かなくていいのはいいですね。

Prismaが提供しているもの

実はPrisma自体がオブジェクトを拡張できるExtention機能を提供してくれています。
なんだよ、早く言ってくれよう。(最初にドキュメントちゃんと読みなさい)

const prisma = new PrismaClient().$extends({
  result: {
    user: {
      fullName: {
        needs: { firstName: true, lastName: true },
        compute(user) {
          return `${user.firstName} ${user.lastName}`
        },
      },
    },
  },
})
const user = await prisma.user.findFirst({
  where: { id: userId }
});
user.fullName;

ドキュメントにそのままのコードが載ってました。

なるほど。まじでこれ書くのかなー(遠い目)。
$extends は連結して書けるので、書くモデルごとにファイルをわけて拡張用コード書いて、importしてきたそれを連結して書くとかはできそう。
メンテナンス性とかも考えると、実装するときはちょっと工夫が必要そうですね。

使う方としては、Prismaが返すオブジェクトにデータが増えたりメソッドが増えたりしているようになるので、素直に使いやすそう。

あと、これをNestJSでどう書くのかというのが地味に厳しいらしい。
NestJSだとPrismaService作って、それをDIで注入するのが一般的だけど、Prisma ExtentionはそのPrismaServiceのインスタンスに対して適用する必要があって、それをどう書くかでめちゃめちゃ議論されてる。
https://github.com/prisma/prisma/issues/18628

関数をまとめたファイルを使う

このあたりのことを調べててこちらのスライドに辿り着いた。
https://speakerdeck.com/qsona/architecture-decision-for-the-next-n-years-at-studysapuri

ここで関数をファイルでまとめる方法が提示されていて、ちょっとびっくりした。
Ruby脳のぼくには全然思いつかなかった方法だけど、なるほど、これでも全然やりたいことはできそう。

import { User } from '@prisma/client'

export function fullName(user: Pick<User, "firstName" | "lastName">): string {
  return `${user.firstName} ${user.lastName}`
}

実際に fullName を使いたいシーンでだけ、これをimportして使う。

import { fullName } from 'user`;
...
const user = this.repository.findById(id);
return fullName(user);

これのいいところは、prismaが返すオブジェクトすべてをclassにしたり関数をマッピングする必要がないところ。
また、必要なカラムだけをPickで指定することでselect指定されたときなどにも柔軟に対応できる。

まとめ

Prismaが返すオブジェクトに対して、そのモデルが持つ機能をいかに定義するかについて調べてみました。
どれも一長一短はありそうですね。使っているフレームワークやチームの状況によって、何を選択するかは変わってきそうです。

Ubie テックブログ

Discussion