2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Fullstack React GraphQL TypeScript Tutorial をやってみた #2 ~MikroORMとTypeGraphQLを組み合わせてPostgreSQLのCRUDを実現するまで~

Last updated at Posted at 2020-11-11

はじめに

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クエリを実行

することが出来ました。

スクリーンショット 2020-11-10 20.20.47.png

下図が今回の全体像です。
expressを使ってAPIサーバーを立ててクライアントとやりとりします。さらにApolloServerをミドルウェアとしてAPIにGraphQLを適用します。前回まではTypeScriptとMikroORMを組み合わせてPSQLテーブルへのSQLクエリを実行していましたが、今回はさらにGraphQLを組み合わせます。
このとき、type-graphqlからimportしたデコレータを用いて、PSQLテーブルのSQLスキーマをAPIのGraphQLスキーマに翻訳します。
このデコレータをresolver/post.tsentities/Post.tsの両方に適用する、という部分が今回のミソです。
スクリーンショット 2020-11-10 20.06.09.png

MikroORMとTypeGraphQLを使ってGraphQLのQueryを実現する

動画の進行とこの記事の解説の順番が前後してしまう部分がありますが、今回のゴールであるブランチのコードを目掛けて解説していくのでご了承ください。
まずはGraphQLのQuery(つまりRestAPIにおけるGet)の方法から説明していきます。

expressのミドルウェアとしてApolloServerを設定する

まずはApolloServerの設定を行います。

src/index.ts
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テーブルとのデータ授受が可能となります。

src/index.ts
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を記述する

ここはかなり重要です。自分は何度も公式ドキュメントを読みましたが、理解に時間がかかりました。

src/resolvers/post.ts
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, {});
  }
...
src/types.ts
import { EntityManager, IDatabaseDriver, Connection } from "@mikro-orm/core";

export type MyContext {
  em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>
}

デコレータのオンパレードで、初学者的にかなり厳しいです。
まず、ここでのresolverの役割を整理すると、

    1. クライアントからのリクエストおよびそれに応じるレスポンスを、GraphQLのAPIで定義
    1. リクエストに応じてPSQLテーブルに対してSQLクエリを実行

となります。
そこで、改めてコードを見ると、上記 1) 2)が実現されていることが分かります。

src/resolvers/post.ts
@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で統合することができました。いま、全体像は下図のようになっています。
スクリーンショット 2020-11-11 14.27.55.png
しかし、下記のentities/Post.tsでPostは「TypeScriptの型」としては定義されていますが、「GraphQLのスキーマの型」としてはまだ定義されておらず、GraphQLとしては、リクエストに対して応答すべきレスポンスの型が分かっていない状態です。

src/enities/Post.ts
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したデコレータを使い、下記のように追記します。

src/entities/Post.ts
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つを併せ持つことができるようになりました。
スクリーンショット 2020-11-11 14.28.03.png

MikroORMとTypeGraphQLを使ってGraphQLのMutationを実現する

次にGraphQLのMutaiton(つまりRestAPIにおけるGet以外)の方法を解説します。とはいえ、先ほどのQueryと大差ありません。
先ほど同様、Mutation()というメソッドデコレータを使うことでMutaionを定義します。
Mutationなので、リクエストにおける入力値が必要となりますが、これを@Arg()というプロパティデコレータで定義します。

src/resolvers/posts.ts
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

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?