2026年9月20日

2026年9月20日

GraphQLとPostgreSQLの連携設定:自動生成型と自作サーバーの選び方・権限・接続の注意点

PostgreSQL のデータを GraphQL で公開する方法は、大きく「Hasura や PostGraphile のように DB スキーマから API を自動生成するツールを使う」か「GraphQL サーバーを自分で書き、リゾルバーから SQL を発行する」かの2通りです。この記事では、両者の違いと選び方、どちらを選んでも必要になる DB 側の権限・接続の設定、自作サーバー(Node.js)の実装例、本番前に入れておきたい制限を説明します。

先に WordPress との関係を整理しておきます。WordPress のコアは MySQL / MariaDB を前提にしており、PostgreSQL は公式にはサポートされていません。WordPress のデータを GraphQL で扱いたい場合は WPGraphQL などのプラグインを使うのが一般的で(WordPressのGraphQL APIの使い方)、この記事の構成とは別物です。この記事は、WordPress とは別のアプリケーションで PostgreSQL を使っている場合を想定しています。

2つの方式の違い

自動生成型(Hasura、PostGraphile など) は、PostgreSQL のテーブル・ビュー・関数を読み取り、クエリやミューテーションを自動で用意します。CRUD 中心のアプリなら短時間で API ができ、スキーマ変更にも追従しやすいのが利点です。一方で、公開範囲の制御をツールの権限設定や PostgreSQL の行レベルセキュリティ(RLS)で正しく組む必要があり、ここを誤ると見せてはいけない行や列が API から読めてしまいます。

自作型(Apollo Server、GraphQL Yoga などのライブラリ+DBクライアント) は、型定義とリゾルバーを自分で書きます。手間はかかりますが、公開するフィールド・業務ロジック・他の API との組み合わせを細かく制御できます。DB の構造をそのまま外に見せたくない場合や、複数のデータソースをまとめたい場合に向いています。

選ぶ目安は次のとおりです。

用途 向いている方式
管理画面や社内ツールなど、テーブルをほぼそのまま操作したい 自動生成型
公開 API で、DB 構造と API の形を切り離したい 自作型
既存の REST API や外部サービスと組み合わせる 自作型(自動生成型にも連携機能はあるが構成が複雑になりやすい)

各ツールはメジャーバージョンで設定方法が大きく変わることがあります。たとえば Hasura は従来の v2 系と新しい世代の製品(DDN)で構成方法が異なり、PostGraphile も v4 と v5 で設定ファイルの形式が違います(2026年10月時点)。導入時は、使うバージョンの公式ドキュメントを基準にしてください。

共通の準備:API 専用の DB ロールを作る

どの方式でも、GraphQL サーバーには専用のロールで接続させ、スーパーユーザーやテーブル所有者のロールは使わないようにします。API が乗っ取られたり、想定外のクエリが通ったりしても、被害をそのロールの権限内にとどめるためです。ロールと権限の基本は PostgreSQLのロールと権限管理 も参照してください。

以下はテーブル所有者(またはスーパーユーザー)で psql から実行する例です。\password は SQL ではなく psql のメタコマンドで、パスワードを画面に表示せずに設定できます。

-- ログイン用ロール
CREATE ROLE graphql_app LOGIN;
\password graphql_app

-- 必要なスキーマとテーブルにだけ権限を与える
GRANT CONNECT ON DATABASE appdb TO graphql_app;
GRANT USAGE ON SCHEMA app TO graphql_app;
GRANT SELECT ON app.products, app.categories TO graphql_app;
GRANT SELECT, INSERT, UPDATE ON app.orders TO graphql_app;

serial の ID 列にはシーケンスの権限も必要

app.orders の id が serial / bigserial で作られている場合、テーブルの INSERT 権限だけでは行を追加できません。ID の採番に使うシーケンスの権限がないため、次のエラーになります。

ERROR:  permission denied for sequence orders_id_seq

シーケンス名は pg_get_serial_sequence() で確認し、USAGE を与えます(USAGE で nextval() と currval() が使えます)。

SELECT pg_get_serial_sequence('app.orders', 'id');
-- → app.orders_id_seq

GRANT USAGE ON SEQUENCE app.orders_id_seq TO graphql_app;

一方、id bigint GENERATED ALWAYS AS IDENTITY のような identity 列では、テーブルの INSERT 権限だけで採番されます(PostgreSQL 17 で確認)。新しく作るテーブルでは identity 列を使うと、権限の付け忘れが起きにくくなります。採番の仕組みは PostgreSQLのシーケンス も参照してください。

接続元と SSL

接続元の制限は pg_hba.conf で行います。GraphQL サーバーのアドレスからこのロールだけを scram-sha-256 で許可し、hostssl で SSL を必須にします(サーバー側で ssl = on と証明書の設定が必要です)。書き方の詳細は pg_hba.confの設定 を参照してください。

hostssl  appdb  graphql_app  10.0.1.20/32  scram-sha-256

pg_hba.conf を変えたら、SELECT pg_reload_conf(); などで設定を再読み込みします。

接続文字列にはパスワードが含まれるため、ソースコードに書かず環境変数やシークレット管理の仕組みから渡します。

# 例(値はダミー)
export DATABASE_URL="postgres://graphql_app:your-password@db.example.internal:5432/appdb"
export PG_CA_FILE="/etc/ssl/certs/db-ca.pem"

sslmode の意味はクライアントによって違うので注意します。psql などの libpq 系では sslmode=require は暗号化だけを行い、サーバー証明書を検証しません(検証するのは verify-ca / verify-full)。一方、次の例で使う Node.js の pg(node-postgres)は、接続文字列の sslmode=require を証明書の検証ありとして扱い、社内 CA の証明書は既定では信頼しません。また、接続文字列に sslmode などを書くと、コードで渡した ssl の設定が置き換えられます。そのため次の例では、接続文字列に sslmode を書かず、CA 証明書をコードの ssl 設定で渡しています。

自作サーバーの例:GraphQL Yoga と node-postgres

Node.js で、GraphQL Yoga と pg(node-postgres)を使った最小構成の例です。商品を ID で1件取得するクエリと、注文を1件追加するミューテーションを持たせています。2026年10月時点の Node.js 22、graphql-yoga 6、graphql 17、pg 8 と PostgreSQL 17 で動作を確認しました。

npm install graphql graphql-yoga pg
// server.mjs
import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';
import { createSchema, createYoga } from 'graphql-yoga';
import pg from 'pg';

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  // PG_CA_FILE に CA 証明書のパスを入れると、証明書を検証したうえで暗号化する
  ssl: process.env.PG_CA_FILE
    ? { ca: readFileSync(process.env.PG_CA_FILE, 'utf8') }
    : false,
  max: 10,                    // このプロセスが張る接続の上限
  idleTimeoutMillis: 30000,
});

const typeDefs = /* GraphQL */ `
  type Product {
    id: ID!
    name: String!
    price: Int!
  }
  type Order {
    id: ID!
    productId: ID!
    qty: Int!
  }
  type Query {
    product(id: ID!): Product
  }
  type Mutation {
    createOrder(productId: ID!, qty: Int!): Order!
  }
`;

const resolvers = {
  Query: {
    product: async (_parent, { id }) => {
      // 値はプレースホルダ $1 で渡し、文字列連結で SQL を組み立てない
      const { rows } = await pool.query(
        'SELECT id, name, price FROM app.products WHERE id = $1',
        [id]
      );
      return rows[0] ?? null;
    },
  },
  Mutation: {
    createOrder: async (_parent, { productId, qty }) => {
      const { rows } = await pool.query(
        'INSERT INTO app.orders (product_id, qty) VALUES ($1, $2) RETURNING id, product_id AS "productId", qty',
        [productId, qty]
      );
      return rows[0];
    },
  },
};

const yoga = createYoga({ schema: createSchema({ typeDefs, resolvers }) });

createServer(yoga).listen(4000, () => {
  console.log('GraphQL server: http://localhost:4000/graphql');
});

この例の app.orders は id serial PRIMARY KEY, product_id bigint NOT NULL, qty int NOT NULL という列を想定しています。起動して、クエリとミューテーションを送ってみます。

node server.mjs
curl -s http://localhost:4000/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{ product(id: \"1\") { id name price } }"}'
curl -s http://localhost:4000/graphql -H 'Content-Type: application/json' \
  -d '{"query":"mutation { createOrder(productId: \"1\", qty: 3) { id productId qty } }"}'

ミューテーションが permission denied for sequence で失敗する場合は、前の節のシーケンス権限が足りていません。

ポイントは2つです。1つ目は、引数を $1 $2 のプレースホルダで渡していることです。GraphQL の引数は外部からの入力なので、SQL に文字列として埋め込むと SQL インジェクションの原因になります。2つ目は、接続を Pool で使い回していることです。リクエストごとに新しく接続すると、接続確立の遅延と PostgreSQL 側の接続数の両方が問題になります。

実際の API では、ミューテーションの前に認証済みユーザーかどうか、数量が妥当かどうかの確認も入れます。

つまずきやすい点

N+1 クエリ

GraphQL では、一覧の各要素に関連データを付けるクエリ(例: 注文一覧と各注文の顧客)を書くと、リゾルバーが要素ごとに SQL を発行し、1回のリクエストで数百回のクエリが走ることがあります。自作型では DataLoader などでまとめて取得する仕組みを入れます。自動生成型は多くの場合 JOIN などにまとめてくれますが、実際に発行される SQL をログで確認しておくと安心です。

接続数の上限

PostgreSQL の max_connections は、サーバー全体の同時接続数の上限です。GraphQL サーバーを複数プロセスや複数コンテナで動かすと、「プール上限 × プロセス数」が合計の接続数になります。足りなくなったら上限をむやみに上げるのではなく、プールの上限を見直すか、PgBouncer のような接続プーラーを間に置きます(PostgreSQLのmax_connections)。PgBouncer のトランザクションモードでは、SET で変えたセッション単位の設定などが期待どおりに動かないことがあるので、使うドライバーやツールの対応状況を確認してください。

公開範囲の制御

自動生成型では、権限設定をしないと多くのテーブルが API に出る構成になりがちです。公開するスキーマを限定し、ユーザーごとに見える行を制御するなら RLS かツールの権限機能で設定します。自作型でも、リゾルバーごとに認証済みユーザーかどうか、そのデータを見てよいかを確認します。

本番前に入れておきたい制限

  • クエリの深さ・複雑さの制限: 入れ子を深くしたクエリ1本で DB に重い負荷をかけられるため、深さや計算量に上限を設けます。使っているサーバーライブラリのプラグインや設定で追加します。
  • イントロスペクションの扱い: スキーマ全体を取得できる機能です。公開 API では本番で無効にする運用がよく取られますが、それだけで安全になるわけではないので、権限制御と合わせて考えます。
  • タイムアウト: PostgreSQL 側で statement_timeout を API 用ロールに設定しておくと、想定外に重いクエリが長時間実行され続けるのを防げます。設定は、そのロールが次に接続したときから効きます。
ALTER ROLE graphql_app SET statement_timeout = '5s';

値は例です。業務上必要な最も重いクエリの実行時間を測ってから決めてください。設定変更はステージング環境で試してから本番に適用します。

まとめ

  • PostgreSQL と GraphQL の連携は、スキーマから自動生成する方式(Hasura、PostGraphile など)と、自作サーバーでリゾルバーから SQL を発行する方式がある。
  • どちらでも API 専用の最小権限ロールで接続し、pg_hba.conf で接続元を絞り、SSL を使う。接続情報は環境変数で渡す。
  • serial の ID 列に INSERT するロールには、シーケンスの USAGE 権限も与える(identity 列なら不要)。
  • sslmode の意味は libpq と node-postgres で違う。node-postgres では CA 証明書を ssl 設定で渡す。
  • 自作型ではプレースホルダで値を渡し、接続はプールで使い回す。N+1 と接続数の上限に注意する。
  • 公開範囲は RLS や権限設定で制御し、クエリの深さ制限とタイムアウトを本番前に入れる。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲