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

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

```
author {
  id
  posts {
    id
  }
}
```

このような構造にする場合、NestJSでは
* ObjectTypeデコレータを使用したクラスのpropertyとして定義する
* ResolveFieldデコレータを使用したメソッドをresolverに定義する
* 上記両方を併用する

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

## ObjectTypeでのオブジェクト定義

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

  @Field(() => [Post])
  posts: Post[];
}
```

```typescript:post.model.ts
@ObjectType()
export class Post {
  @Field(() => Int)
  id: number;

  @Field(() => Author)
  auther: Author;
}
```

```typescript: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 には以下のように出力されます。

```graphql: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` を取得してみたいと思います。

```typescript:auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;
  //model から `posts` propertyを消します。
}
```

```typescript:post.model.ts
@ObjectType()
export class Post {
  @Field(() => Int)
  id: number;
  //今回の説明に直接は関係ないですが、author propertyも消して問題ないです。
}
```

```typescript:author.resolver.ts
import { Parent, Query, Resolver、ResolveField } 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 には以下のように出力されます。

```graphql: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
}
```

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

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

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

### propertyとResolveFieldの型が違う場合

#### ObjectTypeでの定義を適当にしてみる
```typescript:auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  // 適当な型にしてみる
  @Field(() => String)
  posts: Post[];
}
```

```typescript:author.resolver.ts
import { Parent, Query, Resolver、ResolveField } 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 });
  }
}
```

```graphql:schema.gql
type Author {
  id: Int!
  // 変わった
  posts: String!
}
```
<font color="Red">postsを指定するとエラー。レコードの取得処理は走る。</font>

#### ResolveFieldでの定義を適当にしてみる
```typescript:auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field(() => [Post])
  posts: Post[];
}
```

```typescript:author.resolver.ts
import { Parent, Query, Resolver、ResolveField } 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 });
  }
}
```

```graphql:schema.gql
type Author {
  id: Int!
  // 変わらない
  posts: [Post!]!
}
```

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


## propertyとResolveFieldの型が同じ場合
```typescript:auther.model.ts
@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field(() => [Post])
  posts: Post[];
}
```

```typescript:author.resolver.ts
import { Parent, Query, Resolver、ResolveField } 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 });
  }
}
```

<font color="Red">postsを指定してデータが取得できる</font>

## 結論

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