3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJS + GraphQLでの子オブジェクトの定義について

Last updated at Posted at 2023-12-21

前提

子オブジェクトという呼び方が正しいのか怪しいですが、今回は以下のようなオブジェクトがあった場合、postsをauthorの子オブジェクトと呼んでいます。

author {
  id
  posts {
    id
  }
}

このような構造にする場合、NestJSでは

  • ObjectTypeデコレータを使用したクラスのpropertyとして定義する
  • ResolveFieldデコレータを使用したメソッドをresolverに定義する
  • 上記両方を併用する

という選択肢があります。この記事ではこれらにどのような違いがあるのか検証したいと思います。
https://docs.nestjs.com/graphql/quick-start にある設定は済ませてあり、schema.gql に GraphQL 用のschemaファイルが出力できるようになっている前提で進めます。

ObjectTypeでのオブジェクト定義

auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field(() => [Post])
  posts: Post[];
}
post.model.ts
@ObjectType()
export class Post {
  @Field(() => Int)
  id: number;

  @Field(() => Author)
  auther: Author;
}
author.resolver.ts
import { Query, Resolver } from "@nestjs/graphql";

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }
}

この結果、schema.gql には以下のように出力されます。

schema.gql
type Author {
  id: Int!
  posts: [Post!]!
}

まあこれは公式のドキュメント通りにやっているので、別に特別なことはないです。
ただ、ここで注意しなければいけないのは、 posts は必須フィールドなので、resolverの author メソッドの this.authorsService.findOneById の処理内で、author に紐づく posts も含めなければ、posts を取得する際にエラーになる点です。
「いやいや…それならObject定義にそう書いてるんだから別に findOneById メソッド内で join でも何でもして author に紐づく posts も返せば良いじゃん:sweat_smile:」となるかもしれません。
そのような処理にした場合、将来他の処理で 「posts は要らないけどidを基に単一の author レコードを取得したいなあ:thinking:」と なったときに、findOneById で要らない posts まで取得することになるので、無駄でしかありません。
もちろん、用途ごとに取得用のメソッドを定義するというのであれば回避できますが、実際の開発現場でそんな実装方針になることは殆どないと思います。というかなっていないと信じたい。
また、そもそもGraphQLなので、リクエスト時に posts を指定しなければ通信に含めないことは可能です。ただ、この場合もこの書き方だと内部的には posts のレコード取得自体は行われるので、無駄にDBからレコードを取得したことになります。

ReolveFieldでのオブジェクト定義

https://docs.nestjs.com/graphql/resolvers#code-first-resolver
次は ResolveField というデコレータを使って posts を取得してみたいと思います。

auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;
  //model から `posts` propertyを消します。
}
post.model.ts
@ObjectType()
export class Post {
  @Field(() => Int)
  id: number;
  //今回の説明に直接は関係ないですが、author propertyも消して問題ないです。
}
author.resolver.ts
import { Parent, Query, ResolverResolveField } from "@nestjs/graphql";

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  // 本筋には関係ないですが、何も考えないでfindAllの処理を書くとN+1が起きるので注意
  @ResolveField(() => [Post])
  async posts(@Parent() { id }: Author) {
    return this.postsService.findAll({ authorId: id });
  }
}

この結果、schema.gql には以下のように出力されます。

schema.gql
type Author {
  id: Int!
  posts: [Post!]!
}

はい。schema.gqlの出力はObjectTypeの時と一緒です。
「おいおい、じゃあ何が変わったって言うんだい?勘弁してくれよboy:grinning:」と誰もが感じると思います(?)
ResolveFieldの場合、Authorの子オブジェクトとして posts を指定したら、resolverに定義した postsメソッドが呼ばれます。
要するに、ObjectTypeの propertyとして posts を定義していた場合とは異なり、 authors メソッドのthis.authorsService.findOneById の返り値に posts を含めなくて良くなります。
これにより、データ取得時に posts を指定した時のみDBから posts レコードを取得することが可能になります。

※ちなみにQueryとして定義しているわけではないので、Authorを経由せずにposts のみ取得しようと思ってもできません。

// OK
author {
  posts {
    id
  }
}

// NG
posts {
  id
}

ResolveFieldを使用してauthor一覧で関連するpostsも取得したいとなった場合、何も考えないで実装すると、authorごとにpostsメソッドが呼び出されN+1問題が発生します

お遊び

「ちょ待てよ!ObjectType でも ResolveFiled でもオブジェクトを定義できるのであれば、両方で定義したらどうなるんや兄ちゃん?:thinking:」と思うことでしょう。

結論から言うと、どうやらshcema.gqlへの反映はObjectTypeが優先っぽいのと、型定義が一致している場合はちゃんとResolveFieldで定義したメソッドが呼び出されるようです。

propertyとResolveFieldの型が違う場合

ObjectTypeでの定義を適当にしてみる

auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  // 適当な型にしてみる
  @Field(() => String)
  posts: Post[];
}
author.resolver.ts
import { Parent, Query, ResolverResolveField } from "@nestjs/graphql";

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField(() => [Post])
  async posts(@Parent() { id }: Author) {
    return this.postsService.findAll({ authorId: id });
  }
}
schema.gql
type Author {
  id: Int!
  // 変わった
  posts: String!
}

postsを指定するとエラー。レコードの取得処理は走る。

ResolveFieldでの定義を適当にしてみる

auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field(() => [Post])
  posts: Post[];
}
author.resolver.ts
import { Parent, Query, ResolverResolveField } from "@nestjs/graphql";

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  // 適当な型にしてみる
  @ResolveField(() => String)
  async posts(@Parent() { id }: Author) {
    return this.postsService.findAll({ authorId: id });
  }
}
schema.gql
type Author {
  id: Int!
  // 変わらない
  posts: [Post!]!
}

postsを指定してもデータの取得はできないが、エラーにもならず空配列が返る。レコードの取得処理は走る。

propertyとResolveFieldの型が同じ場合

auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field(() => [Post])
  posts: Post[];
}
author.resolver.ts
import { Parent, Query, ResolverResolveField } from "@nestjs/graphql";

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private readonly authorsService: AuthorsService) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField(() => [Post])
  async posts(@Parent() { id }: Author) {
    return this.postsService.findAll({ authorId: id });
  }
}

postsを指定してデータが取得できる

結論

基本的に子オブジェクトはResolveFiledで定義するのが良いと思います。実際に私は今プロジェクトでもResolveFieldで全部定義していて、今のところはObjectTypeのpropertyとして定義したことがないです。
NestJS楽だから流行ると良いですね(?)需要がありそうだったらもっと記事書こうかな。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?