AWS AppSyncでGraphQL【実践編その2  Subscription】

Pocket
LINEで送る

aws-sns

お疲れさまです!フクロウラボ若杉です。

ほんと、時間が過ぎるのが早いですね〜。この1,2年はそうなんですが、一方で、プライベートでも様変わりしているせいか、1年前が3,4年位前のような感じもしている今日このごろです。

さて、前回は、

AWS AppSyncでGraphQL【実践編その1(Vue+Amplify+Cognito)】

ということで、Amplifyを使ってメモアプリを作成しました。

今回は、GraphQLのSubscriptionを使ってサーバープッシュなアプリを作ってみようと思います。

前回作ったメモアプリを改良して作ろうかと思ったのですが、そもそもメモアプリはCognitoでの認証が入って本人しか見られないコンテンツなため、今回のサンプルにはマッチしなさそうだったので、ゼロから普通の掲示板を作って、更新したタイミングで他に閲覧しているユーザーへもリアルタイムに更新をかけると言ったシンプルなものをやってみようと思います。

2ちゃん風掲示板

sample02

懐かしき2ちゃんっぽい掲示板を作ってみました。

前回と同様に、Amplifyを使って作りましたが、前回はvueを使いましたが、今回はあえてReactで作ってみました。

Reactでアプリを作るに当たり、最低限の雛形をcreate-react-appを使いました。

create-react-appについては、下記を参照してください。Reactの環境をコマンド一発でさっくり作ってくれる便利なツールです。

参考:create-react-app

AmplifyによるAppSync側のAPIの作成

AmplifyでGraphQL用のAppSyncのエンドポイントを作成します。前回、Amplifyについては説明したので、予め、amplify configureやamplify initは完了している前提で話を進めます。

※Amplifyについては、前回の記事を参考にしてください。→ AWS AppSyncでGraphQL【実践編その1(Vue+Amplify+Cognito)】

と言っても、やることは、

amplify add api

と、awsへ反映させる

amplify push

のみです。

amplify add apiでは、下記のような選択にしています。

? Please select from one of the below mentioned services GraphQL
? Provide API name: app
? Choose an authorization type for the API API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name Post

amplify pushの対話では、

? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2

という感じでデプロイしました。少し待つと環境が構築されます。

クライアント側の下準備

下記のコマンドを実行して

yarn add aws-amplify aws-amplify-react

index.jsに

import Amplify from "aws-amplify";
import config from "./aws-exports";
Amplify.configure(config);

を追加します。

graphql関連

query系(./src/graphql/queries.js)

export const getPost = `query GetPost($id: ID!) {
  getPost(id: $id) {
    id
    username
    comment
  }
}
`;
export const listPosts = `query ListPosts(
  $filter: ModelPostFilterInput
  $limit: Int
  $nextToken: String
) {
  listPosts(filter: $filter, limit: $limit, nextToken: $nextToken) {
    items {
      id
      username
      comment
    }
    nextToken
  }
}
`;

mutation系(./src/graphql/mutations.js)

export const createPost = `mutation CreatePost($input: CreatePostInput!) {
 createPost(input: $input) {
 id
 username
 comment
 }
}`;
export const updatePost = `mutation UpdatePost($input: UpdatePostInput!) {
 updatePost(input: $input) {
 id
 username
 comment
 }
}`;
export const deletePost = `mutation DeletePost($input: DeletePostInput!) {
 deletePost(input: $input) {
 id
 username
 comment
 }
}`;

subscription系(./src/graphql/subscriptions.js)

export const onCreatePost = `subscription OnCreatePost {
 onCreatePost {
 id
 username
 comment
 }
}`;
export const onUpdatePost = `subscription OnUpdatePost {
 onUpdatePost {
 id
 username
 comment
 }
}`;
export const onDeletePost = `subscription OnDeletePost {
 onDeletePost {
 id
 username
 comment
 }
}`;

App.js

import React, { Component } from 'react';
import { API, graphqlOperation } from "aws-amplify";
import { listPosts } from './graphql/queries';
import { createPost } from './graphql/mutations';
import { onCreatePost } from './graphql/subscriptions';

class App extends Component {
 state = {
  posts: [],
  username: "",
  comment: ""
 }

 async componentDidMount() {
  try {
   const posts = await API.graphql(graphqlOperation(listPosts))
   console.log('posts: ', posts)
   this.setState({ posts: posts.data.listPosts.items })
  } catch (e) {
   console.log(e)
  }

  API.graphql(graphqlOperation(onCreatePost)).subscribe({
   next: (eventData) => {
    console.log('eventData: ', eventData)
    const post = eventData.value.data.onCreatePost
    const posts = [...this.state.posts.filter(content => {
     return (content.username !== post.username)
    }), post]
    this.setState({ posts })
   }
  })
 }

 createPost = async () => {
  if (this.state.username === '' || this.state.comment === '') return

  const createPostInput = {
   username: this.state.username,
   comment: this.state.comment
  }

  try {
   const posts = [...this.state.posts, createPostInput]
   this.setState({ posts: posts, username: "", comment: "" })
   await API.graphql(graphqlOperation(createPost, { input: createPostInput }))
   console.log('createPostInput: ', createPostInput)
  } catch (e) {
   console.log(e)
  }
 }

 onChange = e => {
  this.setState({ [e.target.name]: e.target.value })
 }

 render() {
  return (
   <div className="App">
    {this.state.posts.map((post,idx) => {return <div key={idx} class="comment"><div class="username">名前:{post.username}</div><div>{post.comment}</div></div>})}
    <div class="input-form">
     <div>名前:<input value={this.state.username} name="username" onChange={this.onChange}></input></div>
     <div><textarea onChange={this.onChange} name="comment" cols="50" rows="10">{this.state.comment}</textarea></div>
     <button onClick={this.createPost}>書き込む</button>
    </div>
   </div>
  )
 }
}

 

cssなど細かいコードは省略します。

動作確認

yarn start

で、サーバを起動して

http://localhost:3000/

をブラウザで開けば、動作が確認できます。

GraphQLのsubscriptionの仕組み

今回の肝のGraphQLのsubscriptionにより、サーバープッシュな挙動を手軽に実装できてしまいましたが、そもそもsubscriptionはどのように実装されているのか気になったので、軽く調べてみました。

GraphQLのsubscriptionの技術仕様は下記にて確認できます。

参考:RFC: GraphQL Subscriptions

GraphQLはあくまで規格なので、Subscriptionの機能を何で実装するかは特に記載はありません。

subscriptions_02

subscriptions_03

RFC: GraphQL Subscriptions より引用

イベント発生からクライアントへのPublishは、結局WebSocketを介して実装されています。

ということは、WebSocketを使ったチャットシステムなどの構築を考えている場合、ALB + EC2やAPI Gateway + Lambdaなどの組み合わせが想定されますが、AppSync(GraphQL)という選択肢として考えて良いかと思います。

Subscriptionを使用するためのスキーマについて

下記のようにPostとMutationが定義されているとします。

type Post {
 id: ID!
 username: String!
 comment: String!
}

input CreatePostInput {
 id: ID
 username: String!
 comment: String!
}

input UpdatePostInput {
 id: ID!
 username: String
 comment: String
}

input DeletePostInput {
 id: ID
}

type Mutation {
 createPost(input: CreatePostInput!): Post
 updatePost(input: UpdatePostInput!): Post
 deletePost(input: DeletePostInput!): Post
}

このような場合、

@aws_subscribe(mutations: ["mutation_field_name"])

という記述をSubscriptionに追加することで、フィールドの更新に対する通知をリアルタイムに受け取る事ができます。

type Subscription {
 onCreatePost: Post
 @aws_subscribe(mutations: ["createPost"])
 onUpdatePost: Post
 @aws_subscribe(mutations: ["updatePost"])
 onDeletePost: Post
 @aws_subscribe(mutations: ["deletePost"])
}

先のクライアント側のコードを見てもらってもわかると思いますが、割と簡単にWebSocketを使ったリアルタイムなアプリが作れてしまいます。

参考:AWS AppSync 開発者ガイド » リアルタイムデータ

 

GraphQLにて使用されるスキーマ言語とクエリ言語について、もう少し詳しく触れていきたいと思っていのですが、あまり整理されてないまま記事を書いてしまったせいで、割と長くなってしまったのでまた別の機会に書きたいと思います。

GraphQLと言えば、2007年に公開されたGitHub API v4でGraphQLを採用していたことで、注目を浴びた技術だと思います。

GraphQL API v4

また、2018年11月にGraphQL Foundationが設立されています。

GraphQL Foundation

今まではFacebook中心で開発が進められてきましたが、運営がGraphQL Foundationに移行することで、より中立的にGraphQLが業界標準になるべく様々なサポートがされていくことが予想されます。また、Facebook、GitHub、Netflix、Airbnb、Twitter、Pinterest、Shopify、Yelp、Atlassian、CNBC、Major League Soccer、The New York Times、Audiなどの大手企業に採用されているという実績もみのがせません。

個人的には、AppSyncようなサーバレスなクラウドサービスの台頭もGraphQLを推進していくのではないかと思っています。

とりあえず今回は、AmplifyとReactを使ってGraphQLのSubscriptionを使ってみたよ!ということでなんですが、今後はもっとAppSyncを使いこなしてみようと思います。データリソースもデフォルトのDynamoDBで、リゾルバの変更などやれていないのでリゾルバの設定についていろいろ試してみたいです。またRDSなど他のデータリソースへ変更した場合なども、いろいろ試して調査していきたいと考えています。

Pocket
LINEで送る