db

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

db

db

選定

Selection

  • Postgres を使うなら Neon。
  • SQLite を使うなら Cloudflare D1。
  • ORM とマイグレーションツールは Drizzle に統一する。生 SQL を直接書くのは、Drizzle で表現できない DDL や一括変換に限る。
どちらを選ぶかは、リレーション、トランザクション、同時書き込み、拡張機能の必要性で決める。Workers 上で完結し、書き込みが軽く、SQLite の機能で足りるなら D1。それ以外は Neon。
  • For Postgres, use Neon.
  • For SQLite, use Cloudflare D1.
  • Use Drizzle consistently for both the ORM and migration tooling. Write raw SQL directly only for DDL or bulk transformations that cannot be expressed in Drizzle.
Choose based on the need for relationships, transactions, concurrent writes, and extensions. If the application runs entirely on Workers, writes are lightweight, and SQLite provides sufficient functionality, use D1. Otherwise, use Neon.

接続先を分ける

Separate Connection Targets

アプリケーションの通常動作とスキーマ変更を、別の接続情報で行う。
  • app_application — アプリが実行時に使う。DML のみ。テーブルの作成、変更、削除の権限を持たない。
  • app_migrations — マイグレーションだけが使う。DDL を実行できる。
app_admin のような曖昧な名前を作らない。何ができるロールなのかが名前から判断できなくなり、結果として全権限を持つ接続をアプリが使い続けることになる。用途ごとに1つ、用途がそのまま名前になるロールだけを作る。
環境変数も分ける。
txt
DATABASE_URL            # app_application。アプリの実行時に読む
MIGRATION_DATABASE_URL  # app_migrations。マイグレーション時だけ読む
  • アプリのランタイムに MIGRATION_DATABASE_URL を渡さない。
  • CI やデプロイのマイグレーション工程にだけ MIGRATION_DATABASE_URL を渡す。
  • 読み取り専用の用途が明確にあるなら app_readonly を足す。用途がないなら作らない。
Use separate connection credentials for normal application operations and schema changes.
  • app_application — Used by the application at runtime. DML only. It does not have permission to create, alter, or drop tables.
  • app_migrations — Used only by migrations. It can execute DDL.
Do not create vague names such as app_admin. Such names make it impossible to tell what a role can do, eventually leaving the application using a fully privileged connection. Create only one role for each purpose, with the role name directly reflecting that purpose.
Also separate the environment variables.
txt
DATABASE_URL            # app_application。アプリの実行時に読む
MIGRATION_DATABASE_URL  # app_migrations。マイグレーション時だけ読む
  • Do not provide MIGRATION_DATABASE_URL to the application's runtime.
  • Provide it only to the migration stage in CI or deployments.
  • If there is a clearly defined read-only use case, add app_readonly. Do not create it if there is no such use case.

Neon

Neon

sql
CREATE ROLE app_migrations LOGIN PASSWORD '...';
CREATE ROLE app_application LOGIN PASSWORD '...';

-- スキーマの所有者は app_migrations
ALTER SCHEMA public OWNER TO app_migrations;

GRANT USAGE ON SCHEMA public TO app_application;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_application;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_application;

-- 今後 app_migrations が作るテーブルにも自動で付与する
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_application;
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app_application;
デフォルト権限の付与を忘れると、マイグレーションで追加したテーブルにアプリがアクセスできず、デプロイ後に初めて気づくことになる。
ts
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import * as schema from './schema'

export const db = drizzle(neon(process.env.DATABASE_URL!), { schema })
  • 本番と別に開発用のブランチを作り、開発が本番へ直接つながらないようにする。
  • 接続文字列に含まれる認証情報をログや例外メッセージに出さない。
sql
CREATE ROLE app_migrations LOGIN PASSWORD '...';
CREATE ROLE app_application LOGIN PASSWORD '...';

-- スキーマの所有者は app_migrations
ALTER SCHEMA public OWNER TO app_migrations;

GRANT USAGE ON SCHEMA public TO app_application;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_application;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_application;

-- 今後 app_migrations が作るテーブルにも自動で付与する
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_application;
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app_application;
If default privileges are not granted, the application will be unable to access tables added through migrations, and the issue will only become apparent after deployment.
ts
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import * as schema from './schema'

export const db = drizzle(neon(process.env.DATABASE_URL!), { schema })
  • Create a development branch separate from production so that development is not directly connected to production.
  • Never include authentication information contained in connection strings in logs or exception messages.

Cloudflare D1

Cloudflare D1

D1 には Postgres のようなロールがない。分離は接続経路で行う。
  • アプリは Workers のバインディング経由で読み書きする。
  • マイグレーションは wrangler と API トークン経由で実行する。アプリのバインディングからは DDL を流さない。
  • マイグレーション用のトークンは、対象データベースだけに権限を絞る。
ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'

export const getDb = (env: Env) => drizzle(env.DB, { schema })
sh
npx wrangler d1 migrations apply <DB_NAME> --local
npx wrangler d1 migrations apply <DB_NAME> --remote
ロールで守れない分、レビューと実行経路の制限で担保する。アプリのコードから DDL を実行できる書き方を残さない。
D1 does not have roles like Postgres. Enforce separation through connection paths.
  • The application reads and writes through a Workers binding.
  • Migrations run through wrangler with an API token. Do not execute DDL through the application's binding.
  • Restrict the migration token's permissions to the target database only.
ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'

export const getDb = (env: Env) => drizzle(env.DB, { schema })
sh
npx wrangler d1 migrations apply <DB_NAME> --local
npx wrangler d1 migrations apply <DB_NAME> --remote
Since roles cannot enforce this, use reviews and restrictions on execution paths to ensure separation. Do not leave any mechanism in application code that can execute DDL.

スキーマとマイグレーション

Schema and Migrations

スキーマは Drizzle のスキーマ定義を単一の正とする。DB を直接触って変更しない。
sh
pnpm drizzle-kit generate   # スキーマ差分からマイグレーションファイルを生成する
pnpm drizzle-kit migrate    # 生成済みのマイグレーションを適用する
  • 生成されたマイグレーションファイルは必ず読んでからコミットする。意図しない DROP や型変換が入っていないか確認する。
  • 適用済みのマイグレーションファイルを後から書き換えない。修正は新しいマイグレーションで行う。
  • drizzle-kit push
    は開発中のブランチだけに使う。本番や共有環境に使わない。履歴が残らない。
  • マイグレーションは前方互換にする。新旧のアプリコードが同時に動く時間帯があるため、列の削除やリネームは、追加、二重書き込み、切り替え、削除の複数段階に分ける。
  • 大きなテーブルへのロックを伴う変更は、実行時間とロック範囲を事前に見積もる。
  • 実行前にバックアップまたは復旧手段を確認する。Neon ならブランチ、D1 なら export。
Treat the Drizzle schema definitions as the single source of truth. Do not modify the database directly.
sh
pnpm drizzle-kit generate   # スキーマ差分からマイグレーションファイルを生成する
pnpm drizzle-kit migrate    # 生成済みのマイグレーションを適用する
  • Always read generated migration files before committing them. Check for unintended DROP operations or type conversions.
  • Never modify a migration file after it has been applied. Make corrections in a new migration.
  • drizzle-kit push
    is only for development branches. Do not use it in production or shared environments because it does not preserve history.
  • Keep migrations forward-compatible. Since old and new application code may run at the same time, split column removals and renames into multiple stages: add, dual-write, switch over, and remove.
  • Estimate execution time and lock scope in advance for changes that acquire locks on large tables.
  • Confirm that a backup or recovery method is available before running migrations. Use a branch for Neon or an export for D1.

データ移行

Data Migration

既存レコードの一括変換やスキーマ移行に伴うデータ修正は、次の順で行う。いきなり本番のレコードを書き換えない。
  1. 対象レコード数を取得する API を作る。
  2. 変換を実行してドライランする API を作る。実際には書き込まず、変更されるはずの件数と内容を返す。
  3. 事前に取得した件数とドライランの変更件数が一致するか確認する。一致しなければ修正して再実行する。
  4. 一致したら本番で実行し、その後この API を削除する。
  • API は実行後に必ず削除する。一時的な運用口を残さない。
  • API には認証をかける。公開経路に置かない。
  • 実行結果(件数、開始と終了、失敗したレコード)をログに残す。
  • 件数が多い場合はバッチに分け、途中で失敗しても再実行できるようにする。冪等にするか、処理済みを記録する。
  • 変換前の値を戻せるようにしてから実行する。
For bulk transformations of existing records or data corrections associated with schema migrations, follow this order. Do not immediately overwrite production records.
  1. Create an API that returns the number of target records.
  2. Create an API that runs the transformation as a dry run. It must not write anything; instead, return the number and content of records that would be changed.
  3. Verify that the number retrieved in advance matches the number of changes reported by the dry run. If they do not match, fix the issue and rerun the dry run.
  4. Once they match, run the migration in production and then delete the API.
  • Always delete the API after it has run. Do not leave a temporary operational endpoint in place.
  • Protect the API with authentication. Do not expose it on a public route.
  • Log the execution results, including the record count, start and end times, and any failed records.
  • If the number of records is large, divide the work into batches and make it possible to resume after a mid-process failure. Make the operation idempotent, or record which records have already been processed.
  • Ensure that the original values can be restored before running the transformation.

確定前に確認する

Final Checks

  • Postgres なら Neon、SQLite なら D1 を選んでいるか。
  • アプリの接続が app_application で、DDL 権限を持っていないか。
  • app_admin のような用途の曖昧なロールを作っていないか。
  • DATABASE_URL と MIGRATION_DATABASE_URL が分かれ、アプリのランタイムに後者が渡っていないか。
  • Neon で ALTER DEFAULT PRIVILEGES を設定したか。新しいテーブルにアプリが届くか。
  • D1 でアプリのコードから DDL を実行できる箇所がないか。
  • 生成されたマイグレーションを読んだか。意図しない DROP がないか。
  • 適用済みのマイグレーションファイルを書き換えていないか。
  • 本番に
    drizzle-kit push
    を使っていないか。
  • 列の削除やリネームを、新旧コードが共存する前提で段階に分けたか。
  • データ移行で件数取得とドライランを行い、件数が一致してから実行したか。
  • 移行用 API を削除したか。
  • Have you selected Neon for Postgres and D1 for SQLite?
  • Does the application use app_application without DDL permissions?
  • Have you avoided creating vague roles such as app_admin?
  • Are DATABASE_URL and MIGRATION_DATABASE_URL separated, with the latter not provided to the application's runtime?
  • Have you configured ALTER DEFAULT PRIVILEGES in Neon, and can the application access new tables?
  • Is there any place in the application code where DDL can be executed against D1?
  • Have you read the generated migration and checked for unintended DROP operations?
  • Have you avoided modifying an applied migration file?
  • Have you avoided using
    drizzle-kit push
    in production?
  • Have you staged column removals and renames on the assumption that old and new code will coexist?
  • Did the data migration retrieve the record count and perform a dry run, and did it execute only after the counts matched?
  • Did you delete the migration API?