16
12

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.

ApolloServerを使ってGraphQLのSubscriptionを調査する

Last updated at Posted at 2020-10-15

#はじめに
GraphQLの概要の紹介、QueryとMutationの動きを確認した前回の続きです。
本記事はGraphQLのSubscriptionについての投稿となります。

Subscription(サブスクリプション)とは直訳で「購読」という意味で、月額課金サービスなどの総称で耳馴染みのある言葉ですが、
GraphQLにおいては、Mutationにおいてデータ操作をした際に、クライアント側でサーバーから発行された変更情報を受け取ることを指し、チャットアプリや通知機能、ログイン認証などでの利用が考えられます。
こちらの解説が大変分かりやすいので、オススメです。

今回は前回サーバーを立てたExpress-GraphQLに代わりApolloを利用して、GraphQLサーバーを立てた上でSubscriptionの動きを確認していきたいと思います。

##Apolloとは

Apolloは、データグラフを構築するためのプラットフォームであり、アプリケーションクライアント(ReactやiOSアプリなど)をバックエンドサービスにシームレスに接続する通信レイヤーです。

クライアントとサーバーの中間にあるApolloサーバーによって、GraphQLによるデータのやり取りがスムーズになります。

アーキテクチャ例
download.png

公式ドキュメント

##下準備
プロジェクトを作った後に、npm経由でapollo-serverをインストールします。

mkdir graphql-server
cd graphql-server
mkdir resolver
touch index.js db.js resolver/{Query.js,Mutation.js,Subscription.js}
npm init
npm install apollo-server apollo-server-express --save

##サンプルコードを読みやすくするために

###PubSub

PubSubはSubscriptionによる通知イベントを作成する工場のような存在。

example.js
const { PubSub } = require('apollo-server');

const pubsub = new PubSub();

###gql

gqlというタグ付きテンプレートリテラルと呼ばれる特別な関数を使用して、GraphQLに関連する定義をJavaScripの文字列として表現できるようにします。

example.js
const typeDefs = gql`
  type Subscription {
    postAdded: Post
  }

  type Query {
    posts: [Post]
  }

  type Mutation {
    addPost(author: String, comment: String): Post
  }

  type Post {
    author: String
    comment: String
  }
`

###Resolver引数
リゾルバ関数には固定の役割を持つ4つの引数があります。

引数(昇順) 説明
parent 親リゾルバからの戻り値。
args フィールドに提供されたすべてのGraphQL引数を含むオブジェクト
context 特定の操作に対して実行されるすべてのリゾルバ間で共有されます。これを使用して、認証情報やデータソースへのアクセスなどの操作ごとの状態を共有します。
info フィールド名、ルートからフィールドへのパスなど、操作の実行状態に関する情報

##コードの紹介と解説
作成したファイルにコードを書いていきます。
QueryとMutationについての概要は省略致しますので、はじめてGraphQLに触られる方はこちらの記事からどうぞ!😊

先ず、データベース代わりのJavaScriptファイルを用意します。

db.js
const posts = [{
    id: '1',
    title: 'こころ',
    author: '夏目漱石'
}, {
    id: '2',
    title: '舞姫',
    author: '森鴎外'
}, {
    id: '3',
    title: '羅生門',
    author: '芥川龍之介'
}]

const db = {
    posts,
}

module.exports  = db;

続いてQuery,Mutation,そして今回のテーマであるSubscriptionのリゾルバ関数を書いたファイルを順番に用意していきます。

Query.js
const Query = {
    posts(parent, args, { db }, info) {
       //クエリを書いた時に引数が「ない」時
       //模擬データベースの内容を全て表示
        if (!args.query) {
            return db.posts
       //クエリを書いた時に引数が「ある」時
       //引数とtitle or authorが一致したものだけを表示
        }else{
            return db.posts.filter((post) => {
            const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase())
            const isAuthorMatch = post.author.toLowerCase().includes(args.query.toLowerCase())
            return isTitleMatch || isAuthorMatch
        })
    }
    }
}

module.exports  = Query

クエリのリゾルバ関数です。引数の有無で条件分岐をしています。
JavaScriptで条件を細かく書くことで、「本当に必要な情報だけを取得できること」がGraphQLの強みです。

Mutation.js
const Mutation = {
    createPost(parent, args, { db, pubsub }, info) {
        const postNumTotal = String(db.posts.length + 1)
        const post = {
            id: postNumTotal,
            ...args.data
        }

        //データベース更新
        db.posts.push(post)
        //Subscription着火
        pubsub.publish('post', { 
                post: {
                    mutation: 'CREATED',
                    data: post
                }
             })
        return post
    },
    updatePost(parent, args, { db, pubsub }, info) {
        const { id, data } = args
        const post = db.posts.find((post) => post.id === id)
        if (!post) {
            throw new Error('Post not found')
        }

        if (typeof data.title === 'string' && typeof data.author === 'string') {
            //データベース更新
            post.title = data.title
            post.author = data.author
            console.log(post)
            //Subscription着火
            pubsub.publish('post', {
            post: {
                mutation: 'UPDATED',
                data: post
            }
        })
        }

        return post
    },
    deletePost(parent, args, { db, pubsub }, info) {
        const post = db.posts.find((post) => post.id === args.id)
        const postIndex = db.posts.findIndex((post) => post.id === args.id)

        if (postIndex === -1) {
            throw new Error('Post not found')
        }
        //データベース更新
        db.posts.splice(postIndex, 1)
        //Subscription着火
        pubsub.publish('post', {
                post: {
                    mutation: 'DELETED',
                    data: post
                }
            })
        return post
    },
}

module.exports  = Mutation

Mutationのリゾルバ関数では、データベースの更新とSubscriptionの着火をしています。

Subscription.js
const Subscription = {
    post: {
        subscribe(parent, args, { pubsub }, info) {
            return pubsub.asyncIterator('post')
        }
    }
}

module.exports = Subscription

Subscriptionのリゾルバ関数です。
pubsub.asyncIteratorでサブスクリプションのイベントを非同期でリッスンします。

説明の関係で最後になりましたが、
スキーマの定義とサーバー起動のファイルになります。

index.js
const  {ApolloServer,PubSub,gql} = require('apollo-server');
const db = require('./db')
const Query = require('./resolver/Query')
const Mutation = require('./resolver/Mutation')
const Subscription = require('./resolver/Subscription')

//スキーマ定義
const typeDefs = gql`
type Query {
  posts(query: String): [Post!]!
}

type Mutation {
  createPost(data: CreatePostInput!): Post!
  deletePost(id: ID!): Post!
  updatePost(id: ID!, data: UpdatePostInput!): Post!
}

# Subscription
type Subscription {
  post: PostSubscriptionPayload!
}

input CreatePostInput {
  title: String!
  author: String!
}

input UpdatePostInput {
  title: String
  author: String!
}

type Post {
  id: ID!
  title: String!
  author: String!
}

######################
# Subscriptionで利用
######################

enum MutationType {
  CREATED
  UPDATED
  DELETED
}

# Subscriptionのフィールド
type PostSubscriptionPayload {
  mutation: MutationType!
  data: Post!
}

`
//PubSubのインスタンスを作成,Subscriptionが利用可能に!
const pubsub = new PubSub()

const server = new ApolloServer({
    typeDefs: typeDefs,
    resolvers: {
        Query,
        Mutation,
        Subscription,
    },
    context: {
        db,
        pubsub
    }
})

server.listen().then(({ url, subscriptionsUrl }) => {
    console.log(`🚀 Server ready at ${url}`);
    console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
  });

サーバーを立てる際にスキーマやリゾルバ、PubSubなどを引数に指定しています。
指定した引数はQueryやMutationそしてSubscription、それぞれの処理で利用をしています。

準備が出来たらターミナルから起動させます。

node index.js
🚀 Server ready at http://localhost:4000/
🚀 Subscriptions ready at ws://localhost:4000/graphql

前回同様にGraphQL IDEにてテストをしてみます。

Subscriptionを利用するには、クエリを書いた後にYouTubeの再生ボタンのような矢印をクリックします。
写真のように赤いポーズボタンに変化したら、Subscriptionの起動成功です。

スクリーンショット 2020-10-15 23.00.31.png

先ずはMutationで新しくデータを作ってみます。
スクリーンショット 2020-10-15 23.00.52.png

そのままSubscriptionのタブに移動して、結果を確認します。
スクリーンショット 2020-10-15 23.01.03.png
Apolloサーバーからデータが新しく作成されたことに対する変更情報を受け取ることが出来ました。

データの更新、そして削除についても確認します。

Mutationによるデータの更新。
スクリーンショット 2020-10-15 23.01.53.png

Subscriptionにてデータの更新の確認。
スクリーンショット 2020-10-15 23.01.59.png

Mutationによるデータの削除。
スクリーンショット 2020-10-15 23.02.11.png

Subscriptionにてデータの削除の確認(更新と削除を連続して行っています)

スクリーンショット 2020-10-15 23.02.24.png

以上、Mutationによるデータの更新をSubscriptionを通して確認することが出来ました!!🚀🚀

###おわりに
QueryやMutationと比較してSubscriptionは理解に時間がかかったものの、
実際にコードを書き、GraphQL IDEでテストをすることによって自分の中で腹落ちさせることが出来ました。
次回の記事ではフロントにVueを使った上で、実際のアプリケーションの中でGraphQLを使っていきます!

それでは、また!😊

16
12
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
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?