Chapter 07無料公開

✅Entityに定義を集約する

たった
たった
2021.06.25に更新
このチャプターの目次
アプリケーションコード
src
├── middleware     ... 認証・認可とGraphQLのコンテキスト
├── domain         ... ビジネスロジックの共通化
├── usecases       ... アプリケーションロジック
├── infrastructure ... 外部サービスとのやりとり
>> ├── entities       ... エンティティとGraphQLのフィールド
├── resolvers      ... GraphQLのリゾルバー
└── inversify.config.ts ... 依存性の注入(以下、DI)の設定

このチャプターで使用するライブラリ

  • TypeORM
  • class-validator
  • TypeGraphQL

概要

これら3つのライブラリをまとめて使う理由といえば、まさに表題の通り「Entityに定義を集約する」に尽きます。

現在でも開発に着手する際、モデリングから始めることが多いのではないでしょうか。
そうして描かれたドメインモデルを具体化したものがEntityです。

誤解を恐れずに言えば、TypeORMでいうEntityはドメイン駆動設計でいうドメインオブジェクトと同等と考えて良いと思います。

こちらの表現は訂正させていただきます。ご指摘ありがとうございます!

ドメインオブジェクトEntityに定義を集約するということは、ドメインオブジェクトEntityをドメインオブジェクトEntityたらしめる情報をドメインオブジェクトEntityだけが管理するということです。これによって、ドメインオブジェクトEntityの属性とふるまいを他のコードから切り離すことができます。

実装

小難しい話をしたので簡単なコードで考えてみます。
まずは、こんなユーザを考えてみます。

be/src/entities/types/userType.ts
export enum UserRole {
  Admin = '管理者',
  Staff = '担当者',
}

export interface IUser {
  id: number
  name: string
  email: string
  role: UserRole
}

これを実装するとこうなります。
いきなりなんだこれは...って感じですね。これもTypeScriptです。

be/src/entities/user.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import { Field, ObjectType, ID } from 'type-graphql'
import { MaxLength, IsEnum, IsEmail, IsString } from 'class-validator'
import { IUser } from 'src/entities/types/userType.ts'

@Entity()
@ObjectType()
export class User implements IUser {
  @PrimaryGeneratedColumn()
  @Field(() => ID)
  @IsNumber()
  readonly id: number
  
  @Column({ type: 'varchar', length: 191 })
  @Field()
  @MaxLength(191)
  name: string
  
  @Column()
  @Field()
  @IsEmail()
  email: string
  
  @Column({ type: 'enum', enum: UserRole, default: UserRole.Staff })
  @Field(() => UserRole)
  @IsEnum(UserRole)
  role: UserRole
}

このように実装するとクラス名がテーブル名、プロパティ名がカラム名になります。[1]
Fieldアノテーションをつけることで、GraphQLのFieldでUserを参照できるようになります。

バリデータも実装したので、そのふるまいを決めましょう。

be/src/entities/user.ts
+ import { BeforeInsert, BeforeUpdate } from 'typeorm'

export class User implements IUser {
   /* 略 */

+  @BeforeInsert()
+  async validateInsert(): Promise<void> {
+    for (const error of await validate(this)) {
+      if (error.property === 'id') continue
+      throw new Error(JSON.stringify(error.constraints))
+    }
+  }

+  @BeforeUpdate()
+  async validateUpdate(): Promise<void> {
+    for (const error of await validate(this)) {
+      throw new Error(JSON.stringify(error.constraints))
+    }
+  }
}

これでInsert,Update時にバリデーションエラーをthrowするふるまいを定義できました。

...おっと、そういえばEnumはGraphQLに登録してあげる必要がありました。[2]
ついでにやっておきましょう。

be/src/entities/types/userType.ts
+ import { registerEnumType } from 'type-graphql'
export enum UserRole {
  Admin = '管理者',
  Staff = '担当者',
}
+ registerEnumType(UserRole, {
+   'name': 'UserRole',
+   'description': 'ユーザの権限',
+ })

/* 略 */
脚注
  1. Chapter1 ↩︎

  2. 公式ドキュメント ↩︎