前提知識

SaaS のためのデータベース — Postgres + Drizzle を初心者向けに

SaaS における PostgreSQL と Drizzle ORM の超入門: テーブル/行/リレーション、なぜ Postgres か、TypeScript での型付きスキーマ、クエリ、インデックス、データの安全性、マイグレーション、ローカルと本番の違い。

SaaS のためのデータベース — Postgres + Drizzle を初心者向けに

SaaS を作るなら、ユーザーや注文、投稿などのデータをしまっておく場所(データベース)が必要です。本稿では、定番の PostgreSQL(Postgres)を例に、TypeScript から Drizzle ORM を使って分かりやすく扱う方法を解説します。テーブルと行の超基本、なぜ Postgres が既定として強いか、Drizzle で型付きスキーマを定義/クエリする方法、クエリ/インデックス/データ保護の基礎、ローカル開発と本番の違い、「SQL は必須?」「列を変えたらどうなる?」といった疑問までをカバーします。読み終えるころには、あなたの SaaS における DB の役割を理解し、スキーマを進化させるマイグレーションも自信を持って扱えるようになります。


テーブル/行/リレーション(専門用語なし)

データベースの「テーブル」はスプレッドシートのシートのようなもの。列(カラム)と行(レコード)でできています。

  • テーブル: 行と列で構成されたデータのまとまり(例: Users テーブル)。
  • 行: 1 レコード。Users なら 1 人分のユーザー情報。
  • 列: そのレコードが持つ属性(email, created_at など)。各行は各列に値を持ちます。
  • 主キー: 各行を一意に識別するための ID のような列。
  • リレーション: テーブル同士のつながり。たとえば Sessions テーブルの user_id が Users の 1 行(ユーザー)を参照する、という具合。ID で別テーブルの行を指す、それが関係データベースの要です。

要するに、テーブルは「ものの種類」を、行は「個々の実体」を、リレーションは「それらのつながり」を表します。Excel を思い浮かべるとシンプルです。


なぜ PostgreSQL が既定として優れているか

SaaS の DB として Postgres は強力な既定値とよく言われます [1]

  • 信頼性: 長年の実績があり、ACID 準拠でトランザクションを安全に扱えます。
  • 汎用性と強さ: ユーザー/ブログ/決済など幅広い用途に対応。JSON カラム、全文検索、GIS など高度機能も必要に応じて使えます。
  • スケール: スモールスタートから大規模まで。同じ Postgres をチューニング/スケールアップして数百万ユーザーに耐えられます [1]
  • エコシステム: 自前運用もマネージドサービスも選択肢が豊富(Heroku/AWS/GCP/Azure/Supabase/Neon 等)。ツールや情報も潤沢です。

Drizzle ORM 入門:TypeScript で型付きスキーマ

DB と対話するコードは、毎回生 SQL を書く代わりに ORM を使うと安全で生産的です。Drizzle は TypeScript フレンドリーなモダン ORM。Prisma/Sequelize に近い位置づけですが、型安全と SQL への親和性に重点があります。

Drizzle の大きな特長は「スキーマを TypeScript で定義する」こと。テーブル/カラムをコードで記述し、それを単一の真実源としてクエリやマイグレーションに利用します [3]

  • 単一の真実源: スキーマはコード 1 箇所に集約。定義に基づきクエリ整合性を保ち、マイグレーションも生成できます。
  • 型安全: スキーマが型になるため、存在しない列を参照すればコンパイルで検出。タイプミスや型不一致を早期に防ぎます。
  • 文字列マジック回避: 生 SQL 文字列に比べ、関数/定数経由で構築するため安全性が高いです。

例: ログインセッションを管理する sessions テーブルを定義してみます。

import { pgTable, varchar, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";

export const sessions = pgTable(
  "sessions",
  {
    id: varchar({ length: 255 }).primaryKey(),          // 文字列 ID の主キー
    user_id: varchar({ length: 255 }).notNull(),        // 所有ユーザーの ID(必須)
    token: varchar({ length: 512 }).notNull(),          // 一意なセッショントークン(必須)
    expires_at: timestamp({ withTimezone: true }).notNull(), // 有効期限
    ip_address: varchar({ length: 255 }),               // 任意: IP アドレス
    user_agent: text(),                                 // 任意: UA 文字列
    created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
    updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // 制約とインデックス:
    uniqueIndex("sessions_token_unique_idx").on(table.token),  // トークンの一意性を保証
    index("sessions_user_id_idx").on(table.user_id),           // user_id での検索を高速化
  ]
);

ここでやっていることを要約すると:

  • pgTable("sessions", {...}, (table) => [...]) で Postgres のテーブルを定義します。
  • 列定義では、id は主キー、user_id/token は必須、expires_at は有効期限のタイムスタンプ、created_at/updated_at は自動で now() をデフォルトに付与。
  • 第 3 引数では制約/インデックスを定義。token の一意制約、user_id のインデックスなど。

TypeScript 上で定義されるので、タイプミスは編集時点で気づけます。スキーマがコードとして可視化されるため、チームにとっても把握が容易。Drizzle はこの定義をもとにマイグレーションも管理します。

なぜ Next.js/TypeScript と Drizzle が相性良いか? Drizzle は軽量で、巨大なクエリエンジンや複雑なセットアップを要求しません。既存の Postgres 接続(pg の Pool など)を渡すだけ。エッジ/サーバーレスにも載せやすい直球の SQL トランスパイルです。


Drizzle でのデータ操作(インデックスとデータ安全の基本も)

テーブル定義だけでなく、データを取得/追加/更新/削除する必要があります。Drizzle は型安全な API を提供します。例として「特定ユーザーのクレジット取引履歴を取得」してみます。

SQL なら:

SELECT *
FROM credits
WHERE user_uuid = 'some-user-id'
ORDER BY created_at DESC;
``;

Drizzle の TypeScript では:

```ts
import { db } from "@/db";                      // Drizzle の DB インスタンス
import { credits as creditsTable } from "@/db/schema";  // 定義済みの credits テーブル

const userId = "some-user-uuid";
const userCredits = await db.select().from(creditsTable)
  .where(creditsTable.user_uuid.eq(userId))
  .orderBy(creditsTable.created_at.desc());

読んでの通り、意図に近い表現で、スキーマの型と連動します。結果は credits の 1 行に対応するオブジェクト配列で、型もコンパイル時に分かります。内部では Drizzle がパラメータ化された SQL を生成し、SQL インジェクション耐性も確保されます。

インデックス/制約/安全性の要点:

  • パフォーマンスのためのインデックス: user_id で頻繁に検索するならインデックスが効きます。大規模になるほど必須。
  • 一意制約: email や token、注文番号などはユニーク制約でデータの整合性を守ります。
  • NOT NULL: .notNull() は必須列を表し、欠損データの挿入を DB 自体が拒否します。
  • データ型と安全性: 型に合わない値は挿入できません。コード側も型で守られます。
  • トランザクション: 複数操作を「全部成功 or 全部失敗」にできる安全装置。必要になったら使いましょう。
  • バックアップ: 特に本番は定期バックアップを。誤操作から救ってくれます。マネージド Postgres は自動バックアップが便利です。

ローカル開発と本番データベースの違い

ローカル(手元の開発環境)での DB と、本番(実ユーザーが使う環境)での DB では扱いが異なります。

ローカル開発 DB:

  • 自分のマシンや Docker で動かす DB。好きに試行錯誤できます(破壊的操作も可)。
  • .env などで DATABASE_URL=postgres://... を設定し、アプリから接続。
  • チームでも、基本はダミーデータ/テストデータのみを入れます。
  • 小規模でも問題なし。ただし本番の巨大データではクエリ体感が変わるので、早いうちからインデックス/クエリ最適化の意識を。

本番 DB:

  • 実データの保管庫。安易な実験は厳禁。破損はダウンタイムや恒久的損失に直結します。
  • 多くはマネージドサービス(Heroku/AWS/GCP/Azure/Supabase/Neon など)を利用。バックアップ/アップデート/スケールを肩代わり。
  • 接続文字列は別管理(サーバーの環境変数など)。ローカルの文字列を本番に使い回さないこと。
  • 本番マイグレーション: まずローカル/開発で検証し、デプロイの一環として慎重に適用します。コード側が新列を使う準備が整っているタイミングで実施 [5]
  • 監視/ログ: 規模拡大とともに遅いクエリの監視やログが重要に。
  • セキュリティとバックアップ: 強力なパスワード/ネットワーク制限/アップデート/自動バックアップなど、基本を徹底。

まとめ: ローカルと本番を分け、接続先を環境変数で切り替えましょう。ローカルで生成したマイグレーションを、本番にも同じ順序で適用します。誤ってローカルから本番に接続して操作しないよう細心の注意を。


よくある初心者の疑問

Q: Postgres と Drizzle を使うのに SQL の学習は必須ですか?

A: いきなり必須ではありません。ORM の利点は「まずはコードの API だけで進められる」こと。NoSQL 出身でも親しみやすいです。ただし、長期的には SQL の基礎理解が大きな武器になります [4]。遅いクエリの最適化や、ORM の抽象が足りない場面で役立ちます。データモデリング、SELECT/INSERT/UPDATE/DELETEWHEREJOIN、集計といった基本だけでも身につける価値があります。

Q: 後から列やテーブルを変えたくなったら?(なぜマイグレーションが必要?)

A: ソフトウェアは変化が常です。usersage 列を追加したい、usernamehandle にリネームしたい、addresscityzipcode に分割したい——コード上のスキーマを変えたら、DB 側も合わせる必要があります。その橋渡しがマイグレーションです。マイグレーションは「スキーマを旧版→新版へ変更する手順(SQL)」で、Drizzle は多くの場合これを自動生成できます。

手順のイメージ(Drizzle Kit):

  1. TypeScript のスキーマを編集(列追加/新テーブルなど)。
  2. 生成コマンドを実行(例 npx drizzle-kit generate[5]。現状スキーマとの差分から SQL マイグレーションを作成。リネームっぽい変更は確認を求められることもあります。
  3. 適用コマンドで DB に反映(例 npx drizzle-kit migrate)。
  4. スナップショットが更新され、マイグレーションファイルはリポジトリにコミット(全環境で同じ変更を適用)。

例えば creditscredits 列を INTBIGINT に変更したい場合、スキーマを integer()bigint() に変えただけでは既存 DB は変わりません。生成/適用で ALTER TABLE を流し、型を安全に変更します。危険な型変換は警告されるので、段階的移行が必要な場合もあります。開発では気軽にやり直せても、本番ではバックアップ/リバーシブルな変更/入念な検証が重要です。


次の一歩:Drizzle マイグレーションを触ってみる(CTA)

プロジェクトの README に従い、ローカル DB にマイグレーションを生成/適用してみましょう。

npm run db:generate   # スキーマ差分からマイグレーションを生成
npm run db:migrate    # 生成された SQL を DB に適用

生成されたファイル(drizzle/migrations/ 配下)を開くと SQL が並びます。TypeScript の定義が SQL にどう写像されるかが分かります。続けて、Drizzle でテストデータを insert→select する小さなスクリプトを書き、動作確認してみましょう。意欲があれば、列を 1 本追加して再度 generate/migrate。差分検出から適用までの流れが体に入ります。

これで、リレーショナルの基本(テーブル/行/リレーション)、Postgres を既定に据える理由、Drizzle による開発体験、変更を安全に運ぶマイグレーションの意義を一通り掴めました。堅実なデータ基盤の上で、SaaS の機能開発を進めていきましょう。ハッピーコーディング!


参考