はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
この記事はBen Awad
さんのFullstack React GraphQL TypeScript Tutorialを初学者が進めていく、という内容です。
Ben
さんの動画は本当に質が高く、とても学びが多いのですが、自分のような初学者は躓きが多く、なかなか前に進まなかったので、振り返りのメモとして書きます。
今回の対象
動画の下記内容までです。
0:39:50 Apollo Server Express Setup
0:47:32 MikroORM TypeGraphQL Crud
前回 => https://qiita.com/theFirstPenguin/private/9139c0b2f9e56c9a1e49
#始める前に
ブランチをコピー
動画の内容ごとに細かくブランチを切ってくれています。ありがたや。
まずはブランチをローカルにコピーして、そのブランチに移ります。
git pull origin 3_mikroORM-type-graphql-crud:3_mikroORM-type-graphql-crud
git checkout 3_mikroORM-type-graphql-crud
まずは全体像を把握する。
まだチュートリアルは序盤なのですが、type-graphqlが出てきた辺から初学者的にややこしくなってきます。そのため、まずは全体像を整理してみました。
下図が前回までです。MikroORMを使い、TypeScriptによって
- PSQLテーブルのSQLスキーマを定義
- migrationを実行して、テーブルを作成
- insert などのSQLクエリを実行
することが出来ました。
下図が今回の全体像です。
expressを使ってAPIサーバーを立ててクライアントとやりとりします。さらにApolloServerをミドルウェアとしてAPIにGraphQLを適用します。前回まではTypeScriptとMikroORMを組み合わせてPSQLテーブルへのSQLクエリを実行していましたが、今回はさらにGraphQLを組み合わせます。
このとき、type-graphqlからimportしたデコレータを用いて、PSQLテーブルのSQLスキーマをAPIのGraphQLスキーマに翻訳します。
このデコレータをresolver/post.ts
とentities/Post.ts
の両方に適用する、という部分が今回のミソです。
MikroORMとTypeGraphQLを使ってGraphQLのQueryを実現する
動画の進行とこの記事の解説の順番が前後してしまう部分がありますが、今回のゴールであるブランチのコードを目掛けて解説していくのでご了承ください。
まずはGraphQLのQuery(つまりRestAPIにおけるGet)の方法から説明していきます。
expressのミドルウェアとしてApolloServerを設定する
まずはApolloServerの設定を行います。
const main = async () => {
const orm = await MikroORM.init(microConfig); // MikroORMの初期化
await orm.getMigrator().up(); // テーブルのスキーマを作成してテーブルを作成
// ここまで前回の内容。
const app = express(); // APIサーバーとしてexpressを使う
const apolloServer = new ApolloServer({
schema: await buildSchema({
// ここでGraphQLのスキーマを定義する
}),
});
apolloServer.applyMiddleware({ app }); // ApolloServerをexpressのミドルウェアとして使う。
app.listen(4000, () => {
console.log("server started on localhost:4000");
});
};
main()
resolverの設定をする
上述の全体像に表したとおり、ApolloServerがクライアントからリクエストを受け取り、PQSLテーブルに対してSQLクエリを実行する際にGraphQL-APIを使うために、resolverの設定が必要になります。
contextプロパティの設定をする
まずはcontextプロパティを設定します。
contextプロパティには関数を指定しますが、その返り値にオブジェクトを指定できます。
そのオブジェクト(今回は{em:orm.em}
)は全てのresolverで共有することができます。
このことによって、resolverの中でもMicroORMのメソッドを使ってTypeScriptによるPSQLテーブルとのデータ授受が可能となります。
const main = async () => {
const orm = await MikroORM.init(microConfig); // MikroORMの初期化
...
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver], // resolverを定義。一つのテーブルに対して一つのresolverが対応する。
context: () => ({ em: orm.em }), // contextプロパティを設定する。
}),
});
...
resolverを記述する
ここはかなり重要です。自分は何度も公式ドキュメントを読みましたが、理解に時間がかかりました。
import { Resolver, Query, Ctx, Arg, Mutation } from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
@Resolver()
export class PostResolver {
@Query(() => [Post])
posts(@Ctx() { em }: MyContext): Promise<Post[]> {
return em.find(Post, {});
}
...
import { EntityManager, IDatabaseDriver, Connection } from "@mikro-orm/core";
export type MyContext {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>
}
デコレータのオンパレードで、初学者的にかなり厳しいです。
まず、ここでのresolverの役割を整理すると、
-
- クライアントからのリクエストおよびそれに応じるレスポンスを、GraphQLのAPIで定義
-
- リクエストに応じてPSQLテーブルに対してSQLクエリを実行
となります。
そこで、改めてコードを見ると、上記 1) 2)が実現されていることが分かります。
@Resolver() // ①
export class PostResolver {
@Query(() => [Post]) // ②
posts(@Ctx() { em }: MyContext): Promise<Post[]> { // ③
return em.find(Post, {});
}
...
① 最初の@Resolver
はクラスデコレータで、このクラスがresolverであることを示しています。
② @Query(() => [Post])
は、GraphQLのスキーマにおける返り値の型を示す、メソッドデコレータです。
GraphQLにおけるリクエストはQuery(Get)とMutaiton(Get以外)があるので、@Query
は(RestAPIでいうところの)Getリクエストであることを示しています。また、GraphQLはリクエストに対する返り値の型を厳密に規定する必要があり、ここで返り値がPost型の配列であることを示しています。(ここで、Post型を別途GraphQLスキーマにて定義する必要があるのですが、別途説明します)
③ postsというメソッドを記述しています。これは、PostResolverの中でpostsというクエリを指定すると、em.find(Post,{})
、つまりPost
テーブルの中の全ての要素を返す、というメソッドです。このメソッドの返り値は、TypeScriptによってPromise<Post[]>
であることが規定されており、②と重複して見えますが、②はGraphQLのスキーマをについて・③はTypeScriptの型を意味し、両方規定することが必要です。また、@Ctx()
はプロパティデコレータで、前述のcontextプロパティの中身を引用して使いることができ、それによって、em.find
というMikroORMのメソッドを使うことができます。また、その返り値の型はMyContext
であることを明記しています。
いかがでしょうか?かなり難しくないですか?慣れれば簡単なんでしょうか?
特に、Postは「TypeScriptの型」と「GraphQLのスキーマの型」という二重の意味で使われており、混乱を招きやすいポイントだと感じたので、次にそのことを説明したいと思います。
Entity(テーブル)にGraphQLスキーマの型を追記する
ここまでで、resolvers/port.ts
によってGraphQLのQueryとSQLのクエリをTypeScriptで統合することができました。いま、全体像は下図のようになっています。
しかし、下記のentities/Post.ts
でPostは「TypeScriptの型」としては定義されていますが、「GraphQLのスキーマの型」としてはまだ定義されておらず、GraphQLとしては、リクエストに対して応答すべきレスポンスの型が分かっていない状態です。
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property({ type: "date" })
createdAt = new Date();
@Property({ type: "date", onUpdate: () => new Date() })
updatedAt = new Date();
@Property({ type: "text" })
title!: string;
}
そのため、entities/Post.ts
においてtype-graphqlからimportしたデコレータを使い、下記のように追記します。
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
import { ObjectType, Field } from "type-graphql";
@ObjectType() // GraphQLのスキーマの型として定義する。これでPostクラスは、テーブルのスキーマ・TypScriptの型・GraphQLの型を兼ねる。
@Entity()
export class Post {
@Field() // APIレスポンスとして使用するためには、GraphQLのフィールドであることを明示する。
@PrimaryKey()
id!: number;
@Field(() => String) // GraphQLにDateという型はないので、Stringであることを明示する
@Property({ type: "date" })
createdAt = new Date();
@Field(() => String)
@Property({ type: "date", onUpdate: () => new Date() })
updatedAt = new Date();
@Field()
@Property({ type: "text" })
title!: string;
}
これで、resolverの章で触れた、「Post型を別途GraphQLスキーマにて定義する」ことで、Postクラスは「SQLテーブルのスキーマ」「TypeScriptの型」「GraphQLのスキーマの型」の3つを併せ持つことができるようになりました。
MikroORMとTypeGraphQLを使ってGraphQLのMutationを実現する
次にGraphQLのMutaiton(つまりRestAPIにおけるGet以外)の方法を解説します。とはいえ、先ほどのQueryと大差ありません。
先ほど同様、Mutation()
というメソッドデコレータを使うことでMutaionを定義します。
Mutationなので、リクエストにおける入力値が必要となりますが、これを@Arg()
というプロパティデコレータで定義します。
import { Resolver, Query, Ctx, Arg, Mutation } from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
@Resolver()
export class PostResolver {
...
@Mutation(() => Post, { nullable: true }) // ①
async updatePost(
@Arg("id") id: number,
@Arg("title", () => String, { nullable: true }) title: string, // ②
@Ctx() { em }: MyContext // ③
): Promise<Post | null> {
const post = await em.findOne(Post, { id }); // ③
if (!post) {
return null;
}
if (typeof title !== "undefined") {
post.title = title;
await em.persistAndFlush(post);
}
return post;
}
...
① @Mutation(() => Post, { nullable: true })
は、GraphQLのスキーマにおける返り値の型を示す、メソッドデコレータです。GraphQLにおけるリクエストはQuery(Get)とMutaiton(Get以外)があるので、@Mutaion
は(RestAPIでいうところの)Getリクエスト以外であることを示しています。また、GraphQLはリクエストに対する返り値の型を厳密に規定する必要があり、ここで返り値がPost型であることを示しています。type-graphqlのスキーマはデフォルトでnullを許容しないので、nullの可能性がある場合は{ nullable: true }
を指定します。
② @Arg("title", () => String, { nullable: true }) title: string,
というプロパティデコレータを利用して、GraphQLのMutaion(またはQuery)における入力値を規定しています。TypeScriptsの型とGraphQLの型が必ずしも一致しない場合があり、その場合は() => String
のように明示する必要があります。(今回は両方String型なので必要ないです。)ただし、デフォルト設定がnullを許容しないので、nullの可能性がある場合は、{ nullable: true }
を指定します。また、@Arg("title"
のtitleはGraphQLのスキーマ上で用いられる入力値の名前で、TypeScript上で用いられる引数名と区別することができます。
③ @Ctx() { em }: MyContext
というプロパティデコレータを利用して、src/index.ts
で規定しているcontextプロパティの中身を引用して使いることができ、それによって、em.findOne
というMikroORMのメソッドを使うことができます。また、その返り値の型はMyContext
であることを明記しています。
最後に
今回はMikroORM TypeGraphQL Crud
まで書きました。
上記説明したQueryとMutationができればCRUDが実現できると思います。
チュートリアル全14時間のうち、1時間ちょっと進みました。
先は長い。。。
次回 => https://qiita.com/theFirstPenguin/items/6dde647c01dad4e96e22