Hasura GraphQL Engine:バックエンドコードなしでPostgreSQLからGraphQL APIを自動生成する

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Hasuraに向かわせた問題

6か月前、私はMySQL、PostgreSQL、MongoDBとそれぞれ異なる3つのプロジェクトを同時に抱えていました。それぞれのデータベースには確かな強みがあります。しかしスキーマが変わるたびに、リゾルバー関数、RESTコントローラー、認証ミドルウェアを一斉に更新しなければなりませんでした。ある午後、カラム名を1つ変更しただけで3つのAPIエンドポイントとモバイルアプリが壊れた原因を追うのに4時間かかりました。そのデバッグセッションでようやく、何かを変えなければならないと確信しました。

問題は「どのデータベースが最善か」ではなく、「なぜ100回目のCRUDラッパーを手書きしているのか」でした。

根本原因:APIレイヤーというコスト

従来のバックエンド開発には、私がAPIレイヤー税と呼ぶものが伴います。PostgreSQLには完璧なリレーショナルスキーマがあるにもかかわらず、それを公開するには次のことが必要です:

  • すべてのテーブルとリレーションシップにルートハンドラーまたはリゾルバーを書く
  • フィルタリング、ページネーション、ソートのロジックを個別に実装する
  • 認証ミドルウェアを追加し、行レベルのチェックに接続する
  • リアルタイム更新が必要な場合はWebSocketインフラを構築する
  • スキーママイグレーションのたびにこれらすべてを同期させる

20テーブルのスキーマだと、実際のビジネスロジックを1行も書く前に、ボイラープレートが数千行に膨れ上がります。その大部分は純粋に機械的な作業であり、スキーマに確定的に従うものです。それでも人間が手書きするため、そこにバグが忍び込んでしまうのです。

検討したソリューション

選択肢1:手動REST API(FastAPI / Express)

完全なコントロール、使い慣れたツール、カスタマイズも容易です。しかし保守コストは現実のものです。ALTER TABLEのたびに複数ファイルの変更が発生します。複雑なビジネスルールを持つサービスにはこれを採用しましたが、データ量の多いCRUDには向きません。

選択肢2:PostgREST

PostgRESTはPostgreSQLスキーマから直接REST APIを自動生成します。軽量で高速です。しかしRESTではクライアントがフィールド選択を制御できず、必要なフィールドだけを取得することはできません。また組み込みのリアルタイム機能もありません。GraphQLクライアントを使う読み取り中心のダッシュボードには合いませんでした。

選択肢3:Prisma + Apollo GraphQL Server

この組み合わせにより、型安全なクエリと適切なGraphQLレイヤーが得られます。しかし依然としてリゾルバーを書き、Prismaクライアントを管理し、サブスクリプションを個別に設定する必要があります。素のRESTよりは改善されていますが、ボイラープレートをなくせるわけではありません。

選択肢4:Hasura GraphQL Engine

Hasuraは既存のPostgreSQLデータベースに接続し、クエリ、ミューテーション、サブスクリプションを含む完全なGraphQL APIをコードなしで即座に生成します。パーミッションはUIまたはメタデータYAMLで設定でき、ミドルウェア全体に散らばることはありません。本番環境で6か月使用した結果、大きなバックエンドチームを持たずにデータ駆動型アプリを構築するチームに最も推奨できるものです。

DockerでHasuraをセットアップする

Hasuraをローカルで実行する最速の方法はDocker Composeです。docker-compose.ymlを作成します:

version: '3.6'
services:
  postgres:
    image: postgres:16
    restart: always
    environment:
      POSTGRES_PASSWORD: mysecretpassword
    volumes:
      - db_data:/var/lib/postgresql/data

  graphql-engine:
    image: hasura/graphql-engine:v2.40.0
    ports:
      - "8080:8080"
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
      HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"your-256-bit-secret-here"}'
    depends_on:
      - postgres

volumes:
  db_data:

起動します:

docker compose up -d

http://localhost:8080/console を開き、管理者シークレットを入力します。セットアップ完了です。HasuraはあなたのPostgreSQLインスタンスに接続されました。

テーブルとリレーションシップのトラッキング

Hasuraはテーブルを自動的に公開しません。コンソールまたはAPIを通じてテーブルを「トラック」する必要があります。これは意図的な設計です。公開APIに含めたくない内部テーブル(監査ログ、マイグレーション履歴など)が存在する場合があるためです。

例えば、次のテーブルがあるとします:

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  title TEXT NOT NULL,
  body TEXT,
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

Hasuraコンソールで、Data → public → Track All に移動します。Hasuraは外部キーを検出し、リレーションシップを提案します。user → posts の配列リレーションシップと post → user のオブジェクトリレーションシップを承認します。これで、1回のリクエストでネストしたデータをクエリできます:

query GetUserPosts {
  users {
    email
    posts(where: { published: { _eq: true } }) {
      title
      created_at
    }
  }
}

リゾルバーコードは不要です。リレーションシップのトラバーサル、JOIN、フィルタリングはすべて自動的に行われます。

カスタムミドルウェアなしの行レベルセキュリティ

パーミッションモデルには良い意味で驚かされました。従来の構成では、PostgreSQLのRLSポリシーを直接使用する(スケールで管理が複雑)か、すべてのリゾルバーに認可チェックを書く(エラーが起きやすく見落としやすい)かのどちらかでした。Hasuraは第三の道を取ります。コンソールでロールごとにパーミッションルールを定義すると、データベースにアクセスする前にWHERE句へと変換されます。

例:ユーザーは自分の投稿のみ読み取れるようにする。

コンソールで Data → posts → Permissions → user ロール → Select に移動し、行パーミッションを設定します:

{
  "user_id": { "_eq": "X-Hasura-User-Id" }
}

X-Hasura-User-Id は、Hasuraがリクエストのたびに受け取るJWTトークンから抽出されたクレームです。ログイン済みのユーザーが投稿をクエリすると、Hasuraは自動的に WHERE user_id = '<their-id>' を付加します。クエリに何を指定しても、他のユーザーの投稿は見えません。

カラムレベルの可視性、行数制限、insert・update・deleteそれぞれのルールも、すべて同じインターフェースからロールごとに設定できます。

WebSocket実装なしのリアルタイムサブスクリプション

カスタムバックエンドでWebSocketベースのリアルタイム機能を実装するには、最低でも週末1日がかりになります。接続処理、ハートビート、再接続ロジック――これらはあっという間に積み上がります。Hasuraでは、querysubscription に変えるだけです:

subscription WatchNewPosts {
  posts(
    where: { published: { _eq: true } },
    order_by: { created_at: desc },
    limit: 10
  ) {
    id
    title
    created_at
  }
}

結果セットが変更されるたびにクライアントは更新を受け取ります。Hasuraは設定可能な間隔(デフォルト1秒)でPostgreSQLをポーリングし、差分をWebSocket経由で接続中のクライアントにプッシュします。ライブ通知フィードやダッシュボードに対し、追加インフラを管理することなく機能するリアルタイム機能が手に入ります。

本番環境でのHasura管理

6か月の本番運用から、見た目以上に重要な3つのプラクティスを学びました:

バージョン管理にはHasuraメタデータを使う

すべてのパーミッション、リレーションシップ、トラック済みテーブルはメタデータYAMLファイルとして保存されます。エクスポートしてgitにコミットします:

# Hasura CLIをインストール
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash

# プロジェクトを初期化
hasura init my-project --endpoint http://localhost:8080 --admin-secret myadminsecretkey
cd my-project

# 現在のメタデータをエクスポート
hasura metadata export

# 別の環境にメタデータを適用
hasura metadata apply --endpoint https://staging.example.com

環境のプロモーション(ローカル → ステージング → 本番)が1コマンドで完了します。

管理者シークレットをクライアントに公開しない

管理者シークレットはすべてのパーミッションルールをバイパスします。HASURA_GRAPHQL_ADMIN_SECRET はサーバー環境のみに設定し、クライアント向けのリクエストにはすべてJWTトークンを使用します。認証サービス(Auth0、Supabase Auth、またはカスタムJWT発行者)がHasura固有のクレームを含むトークンを発行します:

{
  "sub": "user-uuid-here",
  "https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": ["user"],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": "user-uuid-here"
  }
}

ビジネスロジックにはActionsを使う

Hasuraはバックエンドコードを完全に置き換えるのではなく、データレイヤーを置き換えます。副作用を伴う操作(メール送信、決済処理、サードパーティAPI呼び出し)には、Hasura Actionsを使用します。カスタムGraphQLミューテーションを定義すると、Hasuraがあなたの書いたHTTPハンドラーに転送します。データミューテーションはHasuraの型安全なレイヤーで処理し、副作用はハンドラー内に留まります。

Hasuraが向くケース・向かないケース

6か月使った結果、正直な感想を言うと、スキーマがプロダクトそのものであるようなデータ重視のアプリケーションにおいて、Hasuraは膨大な時間を節約してくれます。社内ツール、ダッシュボード、管理パネル、標準的なCRUDパターンのモバイルバックエンドがその得意領域です。

データベースとクライアント間に重い変換ロジックが必要な場合は話が別です。RESTに深く投資しており、フロントエンドのクエリをGraphQLに移行する準備ができていないチームにとっては、移行コストがメリットを上回るでしょう。

しかし、チームが週末を「フィルターとページネーション付きのリストエンドポイントをまた作る」ことに費やしているなら、Hasuraをしっかり1日試してみてください。私のPostgreSQLテーブルはトラッキングとパーミッション設定が完了し、2時間以内にReactフロントエンドを提供できるようになりました。その時間を実際のプロダクト機能の開発に充てることができたのです。

Share: