🐯

Prisma ORMとGraphQLを併用する時のモデルの組み方

2024/03/12に公開

概要

NestJSでPrisma ORMとGraphQLを併用する際、Entityとオブジェクトタイプを個別に実装するとオーバーラップする部分が多く、不自然な感じになってしまいます。(私だけか?)この二重管理を避けるアプローチについて検討していきます。
もっと良い方法あるよ!って場合は教えてもらえるとすごく嬉しいです...!

基本方針

私が開発しているプロダクトでは、CQRS(Command and Query Responsibility Segregation)を適用しています。具体的には、Query(参照系処理)ではEntityを利用せずObjectTypeで返却し、Command(更新系処理)ではObjectTypeは利用せずEntityを利用しています。

本プロダクトでは一般的なウェブアプリと同様に、大半の場合においてはRDBMSのテーブル単位でレスポンスを組み立てて返却すれば十分です。そのため、できる限りPrisma ORMの型定義をそのままGraphQLオブジェクトに適用しています。

Prisma Schema

model User {
  id        String    @id @default(uuid()) @map("id")
  username  String    @unique() @map("username")
  email     String    @unique() @map("email")
  createdAt DateTime? @default(now()) @map("created_at")
  updatedAt DateTime? @updatedAt @map("updated_at")

  @@map("users")
}

Entities

Entityは基本的にはテーブルと相当する構成を持っており、Prismaの型定義とズレが発生したときにすぐ検知できるようにしたいので、Prismaの型定義をimplementsする形でEntityを定義しています。

// user.entity.ts
import { Prisma } from "@prisma/client";

export class User extends Prisma.User {
  id: string;
  username: string; 
  email: string;
  createdAt: Date | null;
  updatedAt: Date | null;

  static fromObject(object: Prisma.User) {
    // 実際は安全を考慮しつつ適切に実装する
    const user = new User();
    Object.assign(user, object)
  }

  // Entityの振る舞いをメソッドに実装
}

ObjectTypes

ObjectTypeも同様に、Prismaの型定義をimplementsする形で定義しています。さらに、共通して利用するインターフェースタイプを定義しています。

// user.object.ts
import { Prisma } from "@prisma/client";
import { InterfaceType, ObjectType, ID, Field } from '@nestjs/graphql';

// ObjectTypeで共通利用するインターフェースタイプ
@InterfaceType({ isAbstract: true })
export abstract class Entity {
  @Field(() => ID, { nullable: false })
  id: string;

  @Field(() => Date, { nullable: false })
  createdAt: Date | null;  

  @Field(() => Date, { nullable: false })
  updatedAt: Date | null;
}

@ObjectType("User", { implements: Entity })
export class UserObject extends Entity implements Prisma.User {
  @Field(() => String, { nullable: false })
  username: string;

  @Field(() => String, { nullable: false }) 
  email: string;
}

課題と悩み

前述のEntityは直接データベースにアクセスできる構成にはなっていません。実際には、他のレコードにまたがるバリデーションなどのドメインロジックが存在します。そのようなロジックはEntityには実装できず、UserServiceのようなドメインサービスクラスに実装せざるを得ません。

つまり、Entityに実装するロジックと、ドメインサービスクラスに実装するロジックを分けざるを得ない状況があります。例えば、ユーザー作成時にメールアドレスの重複チェックを行うロジックは、ドメインサービスクラスに実装する必要があります。

もういっそのことEntityクラスなんて定義せずに、振る舞いをすべてドメインサービスクラスに寄せてしまった方がよいのかなーと悶々としています。ムズカシイ。

宣伝

営業製作所株式会社ではエンジニアを募集しています!
日本が誇る製造業をもう一度輝かせるという志に賛同いただけるエンジニアの方がいらっしゃったら、ぜひご応募を!!!
それ以外でも話を聞いてみたいという方がいらっしゃいましたら、カジュアル面談だけでも構いませんので、どうぞお気軽にお声がけください 🙌

https://www.green-japan.com/company/8899/job/241722

Discussion