このサイトは非公式の日本語訳です。Cloudflare・EmDashプロジェクトが運営するサイトではありません。

このページで分かること

  • 5つのデータベース(SQLite、D1、Hyperdrive、PostgreSQL、libSQL)が向いている場合と、それぞれの設定項目
  • D1のリードレプリカ、PostgreSQLのロールに必要な権限と接続プール、Hyperdriveのクエリキャッシュを無効にする理由と、匿名の閲覧だけをキャッシュから返す方法
  • コアマイグレーションと初回のシードの適用のされ方と、環境ごとにデータベースを分けること
難易度
実践
読む時間
12分
このページの目次

デプロイごとに、データベースアダプターを1つ選びます。データベースには、コンテンツモデル、エントリー、ユーザー、設定、プラグインのデータが保存されます。メディアのファイルそのものは、別のストレージのバックエンドに置きます。

概要

データベース 使う場面 実行環境
SQLite 1つのNode.jsのプロセスが永続的なディスクを持つ場合 Node.jsまたはローカルの開発環境
D1 サイトをCloudflare Workersで動かし、CloudflareのSQLを使う場合 Cloudflare Workers
Hyperdrive サイトをWorkersで動かし、既存のPostgreSQLのオリジンを使う必要がある場合 Cloudflare Workers
PostgreSQL 複数のNode.jsのプロセスで1つのデータベースを共有する必要がある場合 Node.js
libSQL Node.jsのデプロイで、リモートのSQLite互換のデータベースが必要な場合 Node.js

Cloudflareテンプレートの既定はD1です。SQLiteはNode.jsで最も簡単な選択肢ですが、書き込みできる永続的なボリュームが1つと、運用としてのデータベースのバックアップが必要です。

本サイトの補足 やさしい解説

WordPressでは、データベースはほぼ常にMySQL(またはMariaDB)です。EmDashでは、サイトをどこで動かすかによってデータベースを選びます。Cloudflare Workersで動かすなら、公式テンプレートの既定であるD1、既存のPostgreSQLを使いたい場合はHyperdriveです。Node.jsで1台のサーバーで動かすならSQLite、複数のサーバーで動かすならPostgreSQL、リモートのSQLite互換のデータベースを使いたい場合はlibSQLです。

SQLite

SQLiteはNode.jsの組み込みのデータベースドライバーを使い、Node.jsのデプロイで最も簡単な選択肢です。

astro.config.mjs
import { sqlite } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: sqlite({ url: "file:./data.db" }),
		}),
	],
});

設定

オプション 説明
url string file: の接頭辞を付けたファイルのパス

ファイルのパス

urlfile: で始まる必要があります。

// Relative path
database: sqlite({ url: "file:./data/emdash.db" });

// Absolute path
database: sqlite({ url: "file:/var/data/emdash.db" });

// From environment variable
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });

Cloudflare D1

D1は、CloudflareのサーバーレスなSQLiteデータベースです。Cloudflare Workersにデプロイする場合に使います。

astro.config.mjs
import { d1 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: d1({ binding: "DB" }),
		}),
	],
});

設定

オプション 既定値 説明
binding string wrangler.jsonc のD1のバインディング名
session string "disabled" リードレプリケーションのモード(下記を参照)
bookmarkCookie string "__em_d1_bookmark" セッションのブックマークに使うCookieの名前

Wranglerのバインディング

wrangler.jsonc

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "emdash-db"
    }
  ]
}

wrangler.toml

[[d1_databases]]
binding = "DB"
database_name = "emdash-db"

Wranglerは、デプロイの間にこのバインディングから、存在しないD1データベースを用意できます。EmDashのマイグレーションは別の工程です。バインディングの一式についてはCloudflareへのデプロイを、マイグレーションの手順書についてはコアDBマイグレーションを参照してください。

リードレプリカ

D1は、世界中に分散したサイトの読み込みの遅延を減らすために、リードレプリケーションに対応しています。有効にすると、読み込みのクエリは、常にプライマリーのデータベースに送られる代わりに、近くのレプリカに振り分けられます。

EmDashは、D1のSessions APIを使ってこれを意識せずに使えるように管理します。session オプションで有効にします。

astro.config.mjs
import { d1 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: d1({
				binding: "DB",
				session: "auto",
			}),
		}),
	],
});

セッションのモード

モード 動作
"disabled" セッションを使いません。すべてのクエリがプライマリーに送られます。既定値です。
"auto" 匿名のリクエストは、最も近いレプリカから読み込みます。認証済みのユーザーは、ブックマークのCookieによって、自分の書き込みを直後に読める一貫性(read-your-writes)を得ます。
"primary-first" "auto" と同じですが、最初のクエリは常にプライマリーに送られます。書き込みが非常に多いサイトで使います。

仕組み

  • 匿名の訪問者には first-unconstrained が使われます。読み込みは、遅延が最も小さくなるように最も近いレプリカに送られます。匿名のユーザーは書き込みをしないため、一貫性の保証は必要ありません。
  • 認証済みのユーザー(編集者、投稿者)には、ブックマークに基づくセッションが使われます。書き込みの後、ブックマークのCookieによって、次のリクエストが少なくともその状態を読めることが保証されます。
  • 書き込みのリクエストPOSTPUTDELETE)は、常にプライマリーのデータベースから始まります。
  • ビルド時のクエリ(Astroのコンテンツコレクション)は、セッションをまったく使わず、プライマリーを直接使います。

libSQL

libSQLは、リモート接続に対応したSQLiteのフォークです。Cloudflare D1を使わずに、リモートのデータベースが必要な場合に使います。

astro.config.mjs
import { libsql } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: libsql({
				url: process.env.LIBSQL_DATABASE_URL,
				authToken: process.env.LIBSQL_AUTH_TOKEN,
			}),
		}),
	],
});

設定

オプション 説明
url string データベースのURL(libsql://... または file:...
authToken string リモートのデータベースで実行時に使う認証トークン(ローカルでは省略可能)
migrationAuthTokenEnv string マイグレーションに使うトークンの変数名(既定値は TURSO_AUTH_TOKEN

ローカルでの開発

開発中は、ローカルのlibSQLのファイルを使います。

database: libsql({ url: "file:./data.db" });

PostgreSQL

PostgreSQLは、本格的なリレーショナルデータベースが必要なNode.jsのデプロイで使えます。

astro.config.mjs
import { postgres } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: postgres({
				connectionString: process.env.DATABASE_URL,
			}),
		}),
	],
});

設定

接続文字列、または個別のパラメーターで接続できます。

// Connection string
database: postgres({
	connectionString: "postgres://user:password@localhost:5432/emdash",
});

// Individual parameters
database: postgres({
	host: "localhost",
	port: 5432,
	database: "emdash",
	user: "emdash",
	password: process.env.DB_PASSWORD,
	ssl: true,
});
オプション 説明
connectionString string PostgreSQLの接続URL
host string データベースのホスト
port number データベースのポート
database string データベース名
user string データベースのユーザー
password string データベースのパスワード
ssl boolean SSLを有効にする
pool.min number プールの最小接続数(既定値は0)
pool.max number プールの最大接続数(既定値は10)
pool.connectionTimeoutMillis number 接続を待つ最大時間(pgの既定値は0で、タイムアウトなし)
pool.idleTimeoutMillis number アイドル状態のクライアントを保持する時間(pgの既定値は10,000ミリ秒)
migrationConnectionStringEnv string マイグレーションに使う接続文字列の変数名(既定値は DATABASE_URL

PostgreSQLに到達できない場合や、プールの接続が空かない場合に、リクエストが待つ時間の上限を設けるには、pool.connectionTimeoutMillis に0以外の値を設定します。プールを閉じるまでアイドル状態のクライアントを開いたままにするには、pool.idleTimeoutMillis0 に設定します。どちらのオプションも、省略するとpgの既定値のままになります。

データベースのロールの要件

EmDashは、自身のPostgreSQLのテーブルを作成し、更新します。コアマイグレーションはシステムのテーブルとコレクションのテーブルを作成・変更し、コンテンツタイプは ec_* のテーブルを作成し、フィールドを追加・削除するとそのコレクションのテーブルが変更されます。そのため、設定したPostgreSQLのロールには、初期設定のときだけでなく、サイトが続く限りスキーマに対する権限が必要です。

EmDashには、基準となるロールを1つ使います。そのロールには次のものが必要です。

  • データベースに対する CONNECT
  • 有効なスキーマに対する USAGECREATE
  • EmDashのすべてのテーブルと関数の所有権(直接の所有、または所有するロールへの INHERIT 付きのメンバーシップによるもの)
  • それらのテーブルに対する SELECTINSERTUPDATEDELETE

スーパーユーザーである必要や、CREATEDBCREATEROLE を持つ必要、拡張機能を作成する必要はありません。PostgreSQLには、テーブルに対する ALTERDROP の権限付与はありません。これらの操作は、オブジェクトの所有者と、その権限を継承するロールのものです。別のロールにテーブルの ALL を付与しても、そのロールは所有者にはなりません。EmDashは SET ROLE を実行しないため、継承なしで設定したメンバーシップでは足りません。

多くのインストールでは、データベースの既存のスキーマ(一般的には public)を使えます。データベースをEmDash専用にする場合は、これが最も簡単な選択肢です。以下の例では、emdash_app がEmDashの接続文字列で使うログイン用のロールです。プロバイダーの既存のロールを使うか、専用のログインを作成します。管理用の接続で、データベース、スキーマ、ロールの名前を自分のものに置き換えて、アクセス権を付与します。

GRANT CONNECT ON DATABASE app TO emdash_app;
GRANT USAGE, CREATE ON SCHEMA public TO emdash_app;

これらの付与で、ロールは新しいオブジェクトを作成できるようになります。既存のテーブルの所有者は変わりません。既存のサイトで所有者が混在している場合は、PostgreSQLの所有者の混在を修復する手順書を使ってください。

EmDashは、PostgreSQLの有効な current_schema() を使います。スキーマを作成したり search_path を設定したりはしないため、デプロイの前に接続を確かめます。

SELECT
  current_database(),
  session_user,
  current_user,
  current_schema(),
  current_setting('search_path');

省略可能:専用のスキーマを使う

EmDashが別のアプリケーションとデータベースを共有する場合や、EmDashのオブジェクトを public から分けたい場合は、専用のスキーマを使います。これは省略可能で、EmDashを最初に設定する前に構成するのが最も簡単です。EmDash専用のデータベースには、別のスキーマは必要ありません。

基準となる emdash_app ロールがすでにある前提で、管理用の接続でそのロールのスキーマを作成し、選択します。

GRANT CONNECT ON DATABASE app TO emdash_app;
CREATE SCHEMA emdash AUTHORIZATION emdash_app;
ALTER ROLE emdash_app IN DATABASE app SET search_path = emdash;

これで既存のインストールが public から移ったり、所有者の混在が修復されたりはしません。既存のサイトは現在のスキーマを使い続け、代わりにPostgreSQLの所有者の混在を修復する手順書を使ってください。

本サイトの補足 やさしい解説

WordPressでは、データベースのユーザーにすべての権限を与えて使うことが一般的です。EmDashをPostgreSQLで使う場合は、EmDashが作るテーブルの「所有者」になっているロールで、ずっと接続し続けることが大切です。途中で別のロールに切り替えると、記事の読み書きはできても、次のEmDashの更新(コアマイグレーション)や、コレクションのフィールドの追加・削除が must be owner of table というエラーで失敗することがあります。

接続プール

アダプターは pg.Pool を使います。デプロイに合わせてプールのサイズを調整します。

database: postgres({
	connectionString: process.env.DATABASE_URL,
	pool: { min: 2, max: 20 },
});

Hyperdrive

hyperdrive() アダプターを使うと、既存のPostgreSQL(またはPlanetScale PostgresのようなPostgres互換)のデータベースを使って、EmDashをCloudflare Workersで動かせます。HyperdriveがCloudflareのネットワーク上で接続をプールして高速化し、EmDashのPostgreSQLの方言がクエリを実行します。

astro.config.mjs
import { hyperdrive, r2 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: hyperdrive({ binding: "HYPERDRIVE" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

要件

  • サイトに pg >= 8.16.3 がインストールされていること(pnpm add pg
  • compatibility_flags: ["nodejs_compat"]
  • compatibility_date >= "2024-09-23"

準備

まず、PostgreSQLのロールを準備します。次に、そのロールの接続文字列を使ってHyperdriveの設定を作成し、Wranglerの設定にバインディングを追加します。

wrangler hyperdrive create emdash-db \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full" \
  --caching-disabled

wrangler.jsonc

{
  "hyperdrive": [
    {
      "binding": "HYPERDRIVE",
      "id": "<your-hyperdrive-id>"
    }
  ]
}

wrangler.toml

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"

設定

オプション 既定値 説明
binding string "HYPERDRIVE" プライマリー(キャッシュ無効)のHyperdriveのバインディング名
cachedBinding string 匿名の読み込みに使う、省略可能なキャッシュ有効のバインディング(下記を参照)
preferUncachedAfterWriteMs number 60000* コンテンツの公開後、このミリ秒の間は、匿名の公開ページの読み込みで binding を優先します(Hyperdriveの max_age に合わせます)
migrationConnectionStringEnv string CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING> emdash migrate が使う、オリジンのPostgreSQLに直接接続するURLを持つ環境変数
max number 5 Worker内の、Hyperdriveへの接続プールの最大サイズ

*既定値の 60000 は、cachedBinding を設定した場合にだけ適用されます。それ以外の場合は無視されます。

匿名の読み込みをキャッシュから返す

管理画面と書き込みには書き込み直後の読み込みの一貫性が必要なため、既定ではHyperdriveのキャッシュを完全に無効にします。しかし、GET または HEAD を使う匿名の公開ページのリクエストは、短い間なら古いデータでも許容できます。このトレードオフを受け入れられる場合は、同じデータベースに対して2つのHyperdriveの設定を使います。1つはキャッシュ無効(プライマリーの binding)、もう1つはキャッシュ有効(cachedBinding)です。EmDashは、匿名の公開ページのリクエストをキャッシュ有効のバインディングに、それ以外のすべてのリクエストをキャッシュなしのプライマリーに振り分けます。

# Primary — caching OFF (used by admin, auth'd requests, writes, migrations)
wrangler hyperdrive create emdash-db \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full" \
  --caching-disabled

# Cached — SAME database role and connection string, caching ON
wrangler hyperdrive create emdash-db-cached \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full"
wrangler.jsonc
{
	"hyperdrive": [
		{ "binding": "HYPERDRIVE", "id": "<caching-disabled-id>" },
		{ "binding": "HYPERDRIVE_CACHED", "id": "<caching-enabled-id>" }
	]
}
astro.config.mjs
database: hyperdrive({ binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" });

これは、Cloudflareがキャッシュについて説明している2つの設定を使う方法です。EmDashは、どのバインディングを使うかをリクエストごとに決めます。

  • 公開サイトのパスへの匿名の読み込みGETHEAD、セッションなし、/_emdash の下ではない)→ キャッシュ有効の cachedBindingただし、コンテンツの公開後の短い間(既定は60秒。preferUncachedAfterWriteMs をHyperdriveの max_age に合わせて設定します)は例外です。この間、EmDashはキャッシュなしの binding を優先し、再構築の際に、まだ古いHyperdriveの結果でエッジやオブジェクトのキャッシュが再び満たされないようにします。
  • 認証済みのリクエスト(編集者、投稿者)→ キャッシュなしの binding
  • 変更のリクエストPOSTPUTPATCHDELETE。匿名のものも含む)→ キャッシュなしの binding
  • /_emdash の下へのすべてのリクエスト(管理画面、初期設定、認証、内部API)→ 匿名の GET であっても、キャッシュなしの binding
  • 実行時のマイグレーションとコールドスタート → 常にプライマリーの binding
  • デプロイで管理するマイグレーションmigrationConnectionStringEnv を使ってオリジンのPostgreSQLに直接接続します。どちらのHyperdriveのバインディングも使いません。
本サイトの補足 やさしい解説

Hyperdriveには、データベースの取得結果を一時的に保存する「クエリキャッシュ」があり、既定で有効です。EmDashでは、管理画面で保存した内容をすぐに読み直すため、このキャッシュが有効だと、保存前の古い内容が返って初期設定が壊れることがあります。そのため、EmDashが使うHyperdriveの設定では、必ず --caching-disabled でキャッシュを無効にします。訪問者向けのページだけキャッシュを使いたい場合は、キャッシュ有効の設定をもう1つ作り、cachedBinding に指定します。

省略可能:キャッシュ用に別のロールを使う

マイグレーション、初期設定、認証済みのリクエスト、明示的な書き込みのリクエストは、常にプライマリーの binding を使います。cachedBinding 用の別のロールには、スキーマの所有権や CREATE は必要ありませんが、CONNECT、スキーマの USAGE、公開サイトが使うすべてのテーブルの SELECT が必要です。

匿名の公開ページの GETHEAD のリクエストも、リダイレクトの利用回数と404を記録することがあります。これらの機能を保つには、キャッシュ用のロールに、_emdash_redirectsUPDATE と、_emdash_404_logSELECTINSERTUPDATEDELETE も必要です。公開ページの GETHEAD の間に書き込みをするプラグインやアプリケーションのコードがある場合は、さらに多くの権限が必要になることがあります。制限したキャッシュ用のロールでサイトをテストしていない場合は、両方のバインディングに同じロールを使ってください。

キャッシュ用のロールは、EmDashが初回のマイグレーションを完了した後に追加します。以下の例では、省略可能な emdash スキーマを使っています。public など、自分の有効なスキーマに置き換えてください。プロバイダーの管理用のロールで、ログインとデータベースの設定を作成します。

CREATE ROLE emdash_cached LOGIN PASSWORD 'replace-with-a-secret';
GRANT CONNECT ON DATABASE app TO emdash_cached;
ALTER ROLE emdash_cached IN DATABASE app SET search_path = emdash;

次に、スキーマとテーブルの所有者である emdash_app として接続し、既存のテーブルとこれから作られるテーブルへのアクセス権を付与します。

GRANT USAGE ON SCHEMA emdash TO emdash_cached;
GRANT SELECT ON ALL TABLES IN SCHEMA emdash TO emdash_cached;
GRANT UPDATE ON emdash._emdash_redirects TO emdash_cached;
GRANT SELECT, INSERT, UPDATE, DELETE ON emdash._emdash_404_log TO emdash_cached;

ALTER DEFAULT PRIVILEGES IN SCHEMA emdash
  GRANT SELECT ON TABLES TO emdash_cached;

cachedBinding を有効にする前に、両方のロールで接続し、同じ current_database()current_schema() が返ることを確かめます。共有のスキーマでは、GRANT SELECT ON ALL TABLES は関係のないテーブルも見えるようにします。代わりにEmDashのテーブルに個別にアクセス権を付与し、コレクションやその他のスキーマのオブジェクトが追加されたときに、その付与を更新してください。

コアマイグレーション

EmDashは、対応しているすべての方言について、既定でコアマイグレーションを自動で実行します。Astroのビルドと同期は、検証済みで秘密情報を含まない .emdash/migrations.json も出力し、emdash migrate はこれをデプロイの前に適用できます。SQLite、libSQL、PostgreSQL、D1、Hyperdriveの背後にあるオリジンのPostgreSQL(直接接続)には、デプロイ用の実行部があります。

対象の認証情報、CIでの直列化、autocheckmanual の実行時のポリシー、不明な記録やD1のあいまいな書き込みからの復旧については、コアDBマイグレーションを参照してください。

PostgreSQLでは、実行時のマイグレーションは設定した接続を通して実行されます。Hyperdriveの実行時のマイグレーションは、常にプライマリーのバインディングを使います。デプロイで管理するHyperdriveのマイグレーションは、オリジンのPostgreSQLに直接接続します。コアマイグレーションは、テーブル、インデックス、関数を作成したり、列や制約を変更・削除したり、既存の行を更新したりすることがあります。接続して行を変更できても、既存のEmDashのオブジェクトを所有していないロールでは足りません。実行時のマイグレーションは初期設定より前に実行されるため、データベースの権限の不足をセットアップウィザードで直すことはできません。

データベースが空(コレクションがない)で、セットアップウィザードが完了していない場合、EmDashは最初の起動時にシードファイルも適用します。シードは .emdash/seed.jsonpackage.json#emdash.seed に書かれたパス、seed/seed.json のうち最初に見つかったものから読み込まれ、コンパイル時にビルドに埋め込まれます。どれもない場合は、組み込みの既定のシードが使われます。既存のデータベースに対するその後の起動では、そのコンテンツには手を加えません。

環境ごとにデータベースを分ける

開発、プレビュー、ステージング、本番に、それぞれ専用のデータベースを用意します。本番に向けたプレビューのデプロイは、運用中のデータに対してコアマイグレーションや、コンテンツモデルを壊すコマンドを実行してしまうことがあります。

Cloudflareでは、D1またはHyperdriveの各バインディングを対応するWranglerの環境の下に定義し、Wranglerのコマンドに --env を渡します。Node.jsでは、実行環境ごとに異なるデータベースのURLを注入します。認証情報は astro.config.mjs ではなく、実行時のシークレットに置きます。