Hasuraで構築したGraphQLをバックエンドとしたSPAアプリケーションの実用性の調査

Hasuraで構築したGraphQLをバックエンドとしたSPAアプリケーションの実用性の調査

概要

Hasuraのチュートリアルがとても良かったのでその紹介をしつついくつかポイントをまとめてみました。

バックエンドのチュートリアル(1時間程度) https://hasura.io/learn/ja/graphql/hasura/introduction/

フロントエンド(React)のチュートリアル(5時間程度) https://hasura.io/learn/graphql/react/introduction/

以下に該当する方はチュートリアルをやってみることをおすすめします。

  • Hasuraが便利とは聞くもののよくわかっていない
  • GraphQL自体も雰囲気でしか知らない
  • Query, Mutation, Subscriptionがよくわかっていない
  • TypeScriptの型の自動生成が便利とは聞くもののよくわかっていない
  • GraphQLに認証・認可をどう組み込むべきかよくわかっていない

このチュートリアルで出来上がるものはこちらです。 https://learn-hasura-todo-app.netlify.app/

このチュートリアルで学べること

  • Hasuraの使い方
  • ReactでのGraphQLの使い方
  • Apollo Clientの概要
  • GraphQLスキーマからのTypeScriptの型自動生成
  • GraphQLでの簡単なCRUD
  • チャットのようなリアルタイム通信処理
  • Hasuraと認証・認可の組み合わせ

この記事で紹介すること

  • Hasuraの概要
  • GraphQLスキーマからのTypeScriptの型自動生成
  • GraphQLでの簡単なCRUD
  • Hasuraと認証・認可の組み合わせ

目次

PR

UZUMAKIではRailsエンジニアを絶賛募集中です。

↓の記事を読んでご興味を持っていただいた方は、

是非応募宜しくおねがいします!

Hasuraとは

Hasuraとは簡単にいうとGraphQLサーバを簡単に構築できるオープンソースのプロダクトです。

この図にある通り、クライアントとDBを繋ぐGraphQLサーバの役割を果たします。

image

(引用: Instant GraphQL APIs on your data | Built-in Authz & Caching)

Hasuraについて日本語でもう少し詳しく知りたい方はこちらの記事がとても参考になります。 Hasura GraphQL Engine を紹介する

Hasura GraphQL Engine

『Hasura』とは厳密にはHasura GraphQL Engineのことを指します。オープンソースとして公開されています。

https://github.com/hasura/graphql-engine

Hasura is an open source product that accelerates API development by 10x by giving you GraphQL or REST APIs with built in authorization on your data, instantly.

(README 訳) Hasuraは、API開発を10倍高速化するオープンソース製品で、データに対する認証機能が組み込まれたGraphQLまたはREST APIを瞬時に提供します。

READMEにもこのような説明がありますが、実際に使ってみたところ確かに低コストでGraphQLサーバを構築できると感じました。

また細かいところでいうと、自前でGraphQLサーバを構築する場合は開発者自身がN + 1問題に対応する必要がありますが、Hasuraでは標準で対応済みなのでそういった意味でも開発コストを抑えられて非常にありがたいです。

Hasura Cloud

https://hasura.io/cloud/

Hasura GraphQL Engineを実際にプロダクトとして使うにはどこかにデプロイする必要がありますが、当然それ専用のインフラを用意する必要があります。

Hasura CloudはHasura GraphQL Engineをクラウド上に構築できるフルマネージドなサービスです。 実際に使ってみましたが本当に30秒ほどでHasuraのGraphQLサーバを構築できました。

無料プランもありますが、本番プロダクトとして使うならStandardプラン($99/月)は必須でしょう。 https://hasura.io/pricing/

個人的な所感ですが、個人開発レベルであればHasura CloudではなくHerokuのほうがベターかもしれません。デプロイもボタン一つでできますし、料金的にもHasura Cloudより安く済みます。また、Render.comに関してはHerokuよりもさらに安いので個人開発では有力な候補の一つになりそうです。

デプロイ方法に関してはREADMEにある「Deploy to Heroku」などのボタンを押すだけです。なお、HasuraのチュートリアルもHerokuへのデプロイを前提として書かれています。 https://github.com/hasura/graphql-engine

image

Hasuraの中身を少し紹介

ここからはHasuraの中身の話です。 Hasuraを使う流れとしてはざっくり以下のようになります。

  1. HasuraをHasura CloudなりHerokuなりにデプロイする
  2. 利用するDBを指定する
  3. テーブルを作る
  4. 必要に応じてリレーションを貼る
  5. 実際にクエリやミューテーションを実行してみる
  6. 必要に応じて認可の設定をする

1. HasuraをHasura CloudなりHerokuなりにデプロイする

これは先述の通りボタン一つでできてしまいます。特に説明することはないかと思うので割愛します。

2. 利用するDBを指定する

Hasuraのデプロイ後にまず行うことはDBの指定です。

既存のDBと接続することもできますし新規に作成することもできます。

image

2022/2月現在以下のDBに対応しています。 下4つはComming Soonとなっています。MySQLは未対応のようです。

image

先述のHerokuへのデプロイボタンを押した場合はAddOnとしてPostgreSQLも同時に作られるのでHerokuのダッシュボードからDatabase URLをコピーし、それをHasura上のDatabase URLに貼り付ければOKです。

3. テーブルを作る

テーブルはかなり直感的に作成できます。

  • 型(integer, text, string, uuid など)
  • NOT NULL制約
  • デフォルト値
  • Unique制約
  • 主キーの指定

などなど画面上でぽちぽち指定していくだけです。

スクショにあるようにUUIDを自動で生成してそれをデフォルト値に指定することなども非常に簡単にできます。

image

参考 users テーブルの作成 | Hasura GraphQL チュートリアル

4. 必要に応じてリレーションを貼る

リレーションには1対1, 1対多, 多対多の3パターンがありますが、 Hasuraのドキュメントではそれぞれ以下の呼称が使われています。

  • One-to-one relationship
  • One-to-many relationship
  • Many-to-many relationship

Guides: Data modelling | Hasura GraphQL Docs

リレーションはRelationshipタブから簡単に設定できます。

image

この画面にあるObject relationships, Array relationshipsの記載ですが、それぞれ以下の意味を持ちます。

Object relationships・・・1対1 Array relationships・・・1対多

Railsに置き換えるとhas_onebelongs_toの関係性であればObject relationshipsを、has_manyの関係性であればArray relationshipsを選択すればOKです。

参考 Hasura の リレーションシップ | Hasura GraphQL チュートリアル

5. 実際にクエリやミューテーションを実行してみる

テーブルの作成までできたら最低限GraphQLサーバの構築は完了です。

Hasuraにはクエリやミューテーションを実行するGUIが用意されているので色々試してみると良いと思います。

APIタブに移り、必要なテーブル・カラムにチェックボックスをいれていくだけでクエリが自動的に組み立てられていきます。実行ボタンを押せば結果が取得できます。

image

ミューテーションに関しても同様です。

image

このクエリをフロントエンドのコードにコピペすればそのまま使えます。

6. 必要に応じて認証・認可の設定をする

認可について

Hasuraには認可の仕組みも用意されています。 roleという概念があり、roleごとにテーブル単位またはカラム単位でのCRUDの制御が可能になっています。

たとえばこのスクショの例で言うと、

image
  • admin roleはCRUD全てが許可されている
  • user roleはinsertとdeleteは不可。selectとupdateは条件によっては可。

という設定になっています。

updateの詳細ですが、「リクエスト内のidがX-Hasura-User-Idと一致している場合許可する」となっています。

image

X-Hasura-User-IdとはAuth0などで認証した際に得られるJWTに含まれるカスタムクレームを指します(後述)。これは自前で実装する必要がありますがチュートリアルにもしっかり書かれています。 HasuraのGUIにはさまざまなマッチャーが用意されており柔軟なパーミッション設定ができるようになっています。

カスタム JWT クレームのルール | Hasura GraphQL チュートリアル

Roles & Session variables | Hasura GraphQL Docs

なお、認可についてはこちらの記事がわかりやすかったので掲載しておきます。 Hasura の Role と Permission について理解する - あ、しんのきです

特に以下の部分はおもしろい挙動だなと感じました。

Role ごとに見えるスキーマそのものが変わる
Hasura の面白い挙動の一つだと思うのですが、Role と Permission が設定されると権限のないテーブルやフィールドはただアクセスできないだけではなくその Role から見みたスキーマから消失します。

たとえばtodosテーブルのselectに、「user_idが自身のidと一致する場合許可する」というパーミッションが設定されていた場合、よしなに自分自身のtodosのみを返してくれます。

また、商用プロダクトにおいては単に自身のポストのみを取得できるような認可だけでなく「同じ組織に所属しているユーザーのポストのみ取得できる」のような認可制御が求められることが多いと思います。

その場合の設定例が以下の記事に書かれており非常に参考になったので紹介しておきます。

参考 https://gist.github.com/GavinRay97/d7b8805078a47e00001e58eb8b1027b9

image

(画像: 上記URLより引用) organization_users → organization → organization_users のように一度organization_usersからorganizationをたどりそこから再びorganization_usersに降りてくるように辿るのがポイントでした。

認証について

認可を行うにあたり、まず認証が必要になります。 Hasura自体には認証の機能はないのでAuth0やFirebaseなどを利用することになります。チュートリアルではAuth0を使った例が紹介されています。

具体的には、サインアップ or 認証完了時のフックでHasura側のusersテーブルにレコードを挿入するような設定をすることになります。

なおその際、カスタムJWTクレームのルールを設定し、認証時に得られるJWTにroleやuser_idの情報が格納されるようにしたりと諸々の事前準備が必要です。

// Auth0側に設定する
function (user, context, callback) {
  const namespace = "<https://hasura.io/jwt/claims>";
  context.idToken[namespace] =
    {
      'x-hasura-default-role': 'user',
      // do some custom logic to decide allowed roles
      'x-hasura-allowed-roles': ['user'],
      'x-hasura-user-id': user.user_id
    };
  callback(null, user, context);
}

カスタム JWT クレームのルール \| Hasura GraphQL チュートリアル

// Auth0側に設定する
function (user, context, callback) {
  const userId = user.user_id;
  const nickname = user.nickname;

  const admin_secret = "xxxx";
  const url = "<https://learn-hasura-backend.herokuapp.com/v1/graphql>";

  request.post({
      headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': admin_secret},
      url:   url,
      body:    `{\\"query\\":\\"mutation($userId: String!, $nickname: String) {\\\\n          insert_users(\\\\n            objects: [{ id: $userId, name: $nickname }]\\\\n            on_conflict: {\\\\n              constraint: users_pkey\\\\n              update_columns: [last_seen, name]\\\\n            }\\\\n          ) {\\\\n            affected_rows\\\\n          }\\\\n        }\\",\\"variables\\":{\\"userId\\":\\"${userId}\\",\\"nickname\\":\\"${nickname}\\"}}`
  }, function(error, response, body){
       console.log(body);
       callback(null, user, context);
  });
}

ユーザとルールの同期

参考までに、このチュートリアルでは以下のようなクレームがJWTに格納されることになります。

{
  "<https://hasura.io/jwt/claims>": {
    "x-hasura-default-role": "user",
    "x-hasura-allowed-roles": [
      "user"
    ],
    "x-hasura-user-id": "google-oauth2|xxxxxxxx"
  },
  "given_name": "大地",
  "family_name": "斉藤",
  "nickname": "hrkedz",
  "name": "斉藤大地",
  "picture": "<https://lh3.googleusercontent.com/a-/AOh14GgsNycp_9U1nWKR2XqBeAcSUEky0aezw1-07BMbtw=s96-c>",
  "locale": "ja",
  "updated_at": "2022-02-12T06:12:00.468Z",
  "iss": "<https://dev-ncxmobyg.jp.auth0.com/>",
  "sub": "google-oauth2|xxxxxxxx",
  "aud": "GSRvrGsSJckg28wAWfN1gAUYuCBYedCI",
  "iat": 1644646321,
  "exp": 1644682321,
  "at_hash": "IlyYzviQsXTo8gcbNP_9Fw",
  "nonce": "xxx"
}

詳細はこちらのセクションを読んでください。 Auth0 アプリの作成 \| Hasura GraphQL チュートリアル

Firebaseでも考え方は同様です。CloudFunctionsを使うことになると思います。

Hasura自体に関する話はここまでです。

型定義の自動生成

こちらはHasuraに限った話ではなく一般的なGraphQLを利用した開発の話になります。

チュートリアルに出てきたgraphql-code-generatorが非常に強力だと感じました。 GraphQL Code Generator

dotansimha/graphql-code-generator

昨今のWebフロントエンドにおいてはTypeScriptを使うのがデファクトスタンダードだと思いますが、いちいち型を定義するのは骨が折れます。

このライブラリを使えばGraphQLのスキーマから自動的に型を生成してくれます。

以下、設定ファイルのサンプルです。

# codegen.yml
overwrite: true
schema: # 対象のGraphQLエンドポイント
  - <https://hasura.io/learn/graphql:>
      headers:
        'x-hasura-admin-secret': secret
documents: # ここに該当するファイル群からGraphQLのクエリやミューテーションをよしなに探してきて型を自動生成してくれる
  - 'src/**/*.ts'
  - 'src/**/*.tsx'
generates:
  src/generated/graphql.tsx:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

チュートリアル中ではApollo Clientを使っていたのですが、たとえば自身のTodoリストを取得する際は以下のように書きます。

import {
  GetMyTodosQuery, // Apollo ClientのuseQueryの型として使える
  Todos, // Todoエンティティの型として使える
} from '../../generated/graphql' // 自動生成されるファイル

// graphql-code-generatorはソースコード中のこの部分を解析して自動的に型生成してくれる
export const GET_MY_TODOS = gql`
  query getMyTodos {
    todos(
      where: { is_public: { _eq: false } }
      order_by: { created_at: desc }
    ) {
      id
      title
      is_completed
    }
  }
`

const TodoPrivateList = () => {
  // ここで自動生成された型(GETMyTodosQuery)を指定している
  const { loading, error, data } = useQuery<GetMyTodosQuery>(GET_MY_TODOS)

  if (loading) {
    return <div>Loading...</div>
  }
  if (error || !data) {
    return <div>Error...</div>
  }

  let filteredTodos = data.todos

  // ここで自動生成されたTodoの型(Pick)を指定している
  const todoList = filteredTodos.map(
    (todo: Pick<Todos, 'id' | 'title' | 'is_completed'>, index: number) => (
      <TodoItem key={'item' + index} index={index} todo={todo} />
    )
  )

  return (
    <Fragment>
      <div className="todoListWrapper">
        <ul>{todoList}</ul>
      </div>
    </Fragment>
  )
}

もちろんミューテーションやサブスクリプションに関しても同様に自動的に型生成されたものを利用できます。

このように、graphql-code-generatorを使えば早く・楽に・正確に型を生成することができます。もはや導入必須のツールなんじゃないかなとも思います。

yarn generateを実行すると自動で型生成を行ってくれるのですが、その際--watchオプションをつけるとファイルの変更を検知して再度自動的に型生成してくれるのも非常に便利でした。 一点注意点として設定ファイル内で.envを参照するには以下のようにDOTENV_CONFIG_PATHなど諸々の指定をする必要があったので知らないハマります。


  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "generate": "DOTENV_CONFIG_PATH=./.env.local graphql-codegen --config codegen.yml --require dotenv/config"
  },

最後に

今回のサンプルアプリは『認証・認可, WebSocket通信, CRUD, ポーリング etc』などの機能が実装されたものでしたが、Hasuraの謳い文句の通り、バックエンド側は本当に30分〜1時間程度で構築することができました。

これをRailsで実装するとなると、さらにインフラの構築も必要になるわけですから数日はかかると思います。

バックエンド側でビジネスロジックを挟みたいケースが多い場合はまた話は別かもしれませんが、今回の要件レベルであればHasuraは十分技術選定の候補になり得ると感じました。

また、Hasura単体でも非常に優れたツールだとは思いますが、どちらかというと他のツールと組み合わせて真価を発揮するものだという印象を受けました。 『Hasuraとgraphql-code-generatorを組み合わせて型を自動生成する。それをApollo Clientでそのまま利用する。』という一連の流れが開発生産性を大きく向上させてくれるような気がします。

参考文献

Hasuraでこんなときどうするの?

100秒でHasuraを理解する|yuko @denver|note

Graat(グラーツ)\-グロース・アーキテクチャ&チームス株式会社

もうAPIを自分で開発するのは古い?Hasuraの強烈な有効性について紹介する \- Qiita