Chapter 14無料公開

✅RelationとFieldResolverを使いこなす

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

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

  • TypeORM
  • TypeGraphQL

関連するオブジェクトを取得するというケースは、どのWebアプリケーションでも発生します。
このチャプターでは、関連するオブジェクトを取得する方法を大きく2つに分けました。
それぞれどのように使い分けるかみていきましょう。

1. Lazyパターン

TypeORMのRelation { lazy: true }

どういう時に使うか?
  • 処理に不要なオブジェクトを取得したくない時。
  • 関連するオブジェクトが紐づく時も紐づかない時もある場合。
  • 多重度が 0..*であることが多い。

実装

「ユーザが複数枚の画像を登録できる」仕様を追加する場合を考えます。
インターフェースはこんな感じ。

be/src/entities/types/UserType.ts
+ interface IPhoto {
+  url: string
+ }

interface IUser {
  id: number
  name: string
  email: string
  role: UserRole
+  photos: Photo[]
}

まずはそれぞれEntityを実装します。

be/src/entities/photo.ts
@Entity()
@ObjectType()
export class Photo implements IPhoto {
  @PrimaryGeneratedColumn()
  @Field(() => ID)
  @IsNumber()
  readonly id: number

  @Column()
  @Field(() => ID)
  @IsNumber()
  readonly user_id: number

  @Column()
  @Field(() => String)
  @IsFQDN()
  url: string
  
  @ManyToOne(() => User, (user) => user.photos)
  @JoinColumn()
  user: User
}

relationには{ lazy: true }オプションを指定します。

be/src/entities/user.ts
@Entity()
@ObjectType()
export class User implements IUser {
  @PrimaryGeneratedColumn()
  @Field(() => ID)
  @IsNumber()
  readonly id: number

  /* 略 */  
  
  @Field(() => [Photo])
  @OneToMany(() => Photo, (photos) => photos.user, { lazy: true })
  photos: Promise<Photo[]>
}

続いてユーザを取得するUsecaseを実装します。
この時{ relation: ['photos'] }を指定する必要はありません。

be/src/usecases/UserUsecase.ts
@injectable()
export class UserUsecase {
  constructor(
    @inject(Connection) private readonly conn: Connection
  ) {
  }

  public async getUsers(options?: FindManyOptions) {
    const users: User[] = await this.conn.manager.find(User, options)
    return users // それぞれのUserはphotosを保持していない
  }
}

そしてQueryを実装するとこうなります。

be/src/resolvers/UserResolver.ts
import { UserUsecase } from 'src/usecases/UserUsecase.ts'

@injectable()
@Resolver(() => User)
export class UserResolver {
  constructor(
    @inject(UserUsecase) private readonly usecase: UserUsecase
  ) {}
  
  @Query(() => [User])
  async users() {
    return await this.usecase.getUsers()
  }

  /* 略 */
  
  @FieldResolver(() => [Photo], { nullable: true })
  async photos(@Root() user: User) {
    return await user.photos
  }
}

そして、Queryを実行すると...

query users {
  id
  photos {
    url
  }
}

画像が取得できるようになります。

{
  "data": [
    {
      "id": "1",
      "photos": [
        { "url": "https://example.com/1" }
      ]
    },
    {
      "id": "2",
      "photos": [
        { "url": "https://example.com/2" },
        { "url": "https://example.com/3" }
      ]
    }
    ...
  ]
}

このパターンではUsecasefindした時点でUserがphotosを保持していないというのがミソです。
処理が長く・複雑になるほど不要なオブジェクトを掴んでいるデメリットが発生します。誤ってオブジェクトを上書きしたり削除したりする可能性があります。

このように処理に不要な関連するオブジェクトを、GraphQLで取得したいときはLazyパターンを使いましょう。

2. Eager or Assertiveパターン

TypeORMのRelation { eager: true } or { relations: [''] }

どういう時に使うか?
  • 処理に必須なオブジェクトを取得するとき。
  • 関連するオブジェクトが必ず紐づく場合
  • 多重度が 1 or 0..1であることが多い

実装

ユーザに法人と個人がある場合を考えます。法人番号など一部の属性は個人では必要ありません。
これを以下のようにモデリングしたと考えます。
この時、Userは抽象エンティティとなり、具象エンティティとして法人と個人が存在することになります。
ERD

Userでは以下のようにrelationを貼ることができます。

be/src/entities/User.ts
@Entity()
@ObjectType()
export class User implements IUser {
  /* 略 */  

  @OneToOne(() => Corporate, (corporate) => corporate.seller, {
    nullable: true,
    eager: true, // このオプションを付けない場合は{relations:[]}で指定して取得する
  })
  @Field(() => Corporate, { nullable: true })
  corporate: Corporate | null

  @OneToOne(() => Individual, (individual) => individual.seller, {
    nullable: true,
    eager: true,
  })
  @Field(() => Individual, { nullable: true })
  individual: Individual | null
}

続いてユーザを取得するUsecaseを実装します。
上記のように{ eager: true }オプションを設定している場合、{ relation: ['corporate', 'individual'] }を指定する必要はありません。

be/src/usecases/UserUsecase.ts
@injectable()
export class UserUsecase {
  constructor(
    @inject(Connection) private readonly conn: Connection
  ) {
  }

  public async getUsers(options?: FindManyOptions) {
    const users: User[] = await this.conn.manager.find(User, options)
    return users // それぞれのUserはcorporate/individualを保持している
  }
}

Queryを実装するとこうなります。
この場合、usecaseでオブジェクトを取得しているのでFieldResolverは不要です。

be/src/resolvers/UserResolver.ts
import { UserUsecase } from 'src/usecases/UserUsecase.ts'

@injectable()
@Resolver(() => User)
export class UserResolver {
  constructor(
    @inject(UserUsecase) private readonly usecase: UserUsecase
  ) {}
  
  @Query(() => [User])
  async users() {
    return await this.usecase.getUsers()
  }

  /* 略 */

}

そして、Queryを実行すると...

query users {
  id
  corporate {
    corporateNumber
    presidentName
  }
  individual {
    ownerName
  }
}

個人か法人が取得できるようになります。

{
  "data": [
    {
      "id": "1",
      "corporate": {
        "corporateNumber": "123467890",
	"presidentName": "代表取締役 太郎"
      },
      "individual": null
    },
    {
      "id": "2",
      "corporate": null,
      "individual": {
        ownerName: "個人事業主 二郎"
      }      
    }
    ...
  ]
}

このパターンではUsecasefindした時点でUserがcorporate/individualを保持しているというのがミソです。抽象エンティティのような、関連オブジェクトがないと扱うことができないオブジェクトを処理する場合はEager or Assertiveパターンを採用しましょう。