「初めてのGraphQL」の内容の実装をNestjsでやってみた
「初めてのGraphQL」という書籍を読んだのですが、その中で出てくる、写真投稿アプリ?の実装を Nestjs,TypeScript で実装してみました。
実装をする際のポイントとなりそうなところをメモ的に書いていきたいと思います。自分と同じように、「初めてのGraphQL」の内容をNestjsでやってみようかなぁと思っている方の参考になれば嬉しいです。
ソースコードはGitHubにあげており、以下のやつのbackend-nestjsのディレクトリです。そちらも参考にしていただけますと、幸いです。
※実装していた時、node.jsのバージョンはv16.13.1でした。
コードファーストかスキーマファーストか
コードファーストはコードの内容を元にスキーマファイルを作成してくれるアプローチで、スキーマファーストはスキーマの内容を正として、中のコードを開発者が実装していきます。
コードファーストのメリットはこちらの記事が大変参考になります。
この記事を読んだ影響もあり、コードファーストで実装してみました。
コードファーストな実装の仕方
どうやってコードファーストで実装するかを書きます。
以下のapp.module.ts
のautoSchemaFile
の設定をします。
autoSchemaFile
に設定されたpathに、自動的にスキーマファイルが生成されるようになります。
// ※関係なさそうなところは割愛してます
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: path.join(process.cwd(), "src/schema.gql"),
// sortSchema: true, これするとabcd順というか辞書順になるようです
}),
],
})
export class AppModule {}
そうすることで、デコレーターをもとにschema.gql
を自動で更新してくれるようになります。
以下を見ると、user.resolver.ts
のallUsers
のQueryをコメントアウトすると、schema.gql
のallUsers
というQueryが消えてます。
photoからuserを参照するなど別のモデルを参照したい時
例えば以下のようなqueryで、postedBy
には、User
が入る時のイメージです。
query allPhotosQuery {
allPhotos {
id
name
description
postedBy{
githubLogin
}
}
}
このように別のモデルを参照したい時には、ResolveField
のデコレーターを使うことで、一応は取得することができます。
//* 関係なさそうなところは省略しております。
@Resolver((of) => PhotoModel)
export class PhotosResolver {
constructor(private photoService: PhotoService,private userService: UserService) {}
@Query(() => [PhotoModel], { name: 'allPhotos', nullable: true })
async allPhotos() {
const result = await this.photoService.allPhoto()
return result
}
// ここでuserを取得しにいっている
@ResolveField('postedBy', returns =>UserModel)
async getPostedBy(@Parent() photo: Photo) {
const { userId } = photo;
return this.userService.findOne({githubLogin: userId});
}
}
ただ、これだとphotoの数だけ、userを取得しにいくことになり、N+1問題になります。
N+1問題に対処するにはDataloaderを用いるなどの方法がある様です。
今回の「初めてのGraphQL」の中の実装では触れられていなかったので、実装については割愛させていただきます。
以下の記事とかが参考になりそうです。
MongoDBとの繋ぎこみ
@nestjs/mongoose
mongoose
をインストールします。
yarn add @nestjs/mongoose mongoose
そして、app.module.ts
に設定を記載します。
// ※関係なさそうなところは割愛してます。
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forRoot("ここにdbのホスト")],
})
export class AppModule {}
photoのMongoDB用のスキーマを定義します。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type PhotoDocument = HydratedDocument<Photo>;
@Schema()
export class Photo {
@Prop()
name: string;
@Prop()
category: number;
@Prop()
url:string;
@Prop()
description: string;
@Prop()
userId:string;
@Prop()
created:string
}
export const PhotoSchema = SchemaFactory.createForClass(Photo);
PhotoService
の中でPhoto
の取得などを行っており、以下の様に書きます。
@Injectable()
export class PhotoService {
constructor(@InjectModel(Photo.name) private photoMongoModel: Model<PhotoDocument>) {}
// 全件取得のメソッド
async allPhoto(): Promise<Photo[]> {
const result = this.photoMongoModel.find().exec()
return result
}
}
PhotoService
でPhotoModel
関連を使うために以下の様に、MongooseModule.forFeatureを用いて、modelの定義をする必要があります。
import { Module } from '@nestjs/common';
import {PhotosResolver} from "./photo.resolver";
import {PhotoService} from "./photo.service";
import {UserService} from "@/components/users/user.service";
import {Photo,PhotoSchema} from "@/components/photos/schemas/photo.schema";
import {MongooseModule} from "@nestjs/mongoose";
import {User, UserSchema} from "@/components/users/schemas/user.schema";
import {HttpModule} from "@nestjs/axios";
@Module({
imports: [
HttpModule,
//ここ
MongooseModule.forFeature(
[
{ name: Photo.name, schema: PhotoSchema },
{ name:User.name, schema: UserSchema }
]
)],
providers:[PhotosResolver,PhotoService,UserService]
})
export class PhotosModule {}
これがないと、Error: Nest can't resolve dependencies of the PhotoService (?). Please make sure that the argument PhotoModel at index [0] is available in the PhotosModule context.
というエラーがでます。
MongoDBのクラウドサービス
または私は今回の実装をするに当たり、以下のサービスを使いました。面倒な環境構築も必要ないので、手軽に試せておすすめです。
Subscriptionの実装
SubscriptionとはGraphQLサーバーにデータ変化などの特定のイベントが生じるたびにクライアント側に通知(データ)を送る機能のことです。
チャットなどのリアルタイム性が求められるものに使われるかもしれません。
以下の実際に動かした動画です。
左側はnewUser
をSubscription
しているのですが、右側でUser
を追加するMutation
を行いました。
その際に、左側で追加されたことがリアルタイムで通知されていることがわかるかと思います。
それでは実装に入っていきます。
graphql-subscriptions
をインストールします。
yarn add graphql-subscriptions
app.module.ts
に設定を追加します。
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
+ installSubscriptionHandlers: true,
})
export class AppModule {}
user.resolver.ts
を修正していきます。
まずは、Subscriptionする側の実装です。
+const pubSub = new PubSub();
@Resolver((of) => UserModel)
export class UsersResolver {
constructor(private userService: UserService,private photoService: PhotoService) {}
+ @Subscription((returns) => UserModel,{name:'newUser'})
+ newUser() {
+ return pubSub.asyncIterator('newUser');
+ }
@ResolveField('postedPhotos', returns => [PhotoModel])
async getPosts(@Parent() user: UserModel) {
const { githubLogin } = user;
return this.photoService.findAll({ userId: githubLogin });
}
}
そして通知する側の実装です。
@Resolver((of) => UserModel)
export class UsersResolver {
constructor(private userService: UserService,private photoService: PhotoService) {}
@Mutation(()=>[UserModel],{name:'addFakeUsers',nullable:true})
async addFakeUsers(@Args('count') count:number){
const result = await this.userService.addFakeUsers(count)
result.forEach((createdUser)=>{
+ pubSub.publish('newUser',{newUser:createdUser})
})
return result
}
@ResolveField('postedPhotos', returns => [PhotoModel])
async getPosts(@Parent() user: UserModel) {
const { githubLogin } = user;
return this.photoService.findAll({ userId: githubLogin });
}
}
注意点としては、
pubSub.asyncIterator
の引数にトリガー名を渡すのですが、そこがpubSub.publish
の第一引数のトリガー名と一緒になっていないといけないことです。
また、publishの第二引数がasyncIterator
の戻り値になります。
最後に
「初めてのGraphQL」の写真投稿アプリの内容をNestjsで実装する際のポイントを書いてみました。
また本の内容は、GraphQLの概要をつかむには良かったと思います。
私自身、GraphQLもNestjsもまだ勉強中の身ですが、この記事が誰かの役に立ったら嬉しいです。
Discussion