前提知識

SaaSのためのデータベース: Postgres + Drizzle をやさしく解説

PostgreSQLとDrizzle ORMの初心者向けガイド。テーブル/行/関係、Postgresを選ぶ理由、TypeScriptの型付きスキーマ、クエリ、インデックス、データ安全性、移行、ローカルと本番の違い。

SaaSのためのデータベース: Postgres + Drizzle をやさしく解説

SaaSを作るにはデータの置き場所が必要です。ユーザー、注文、投稿、セッションなどを保存するのがデータベースです。このガイドでは PostgreSQL(Postgres)をデフォルトとして紹介し、Drizzle ORM を使って TypeScript でスキーマ定義とクエリを行う方法を説明します。

学べること:

  • テーブル、行、列、リレーションの意味
  • Postgresがデフォルトに向く理由
  • Drizzleで型付きスキーマを定義する方法
  • インデックスと制約がデータを守る理由
  • ローカルと本番の違い
  • マイグレーションの重要性

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

データベースのテーブルはスプレッドシートのようなものです。

  • テーブル: データの集合(例: users
  • 行: 1件のデータ(1ユーザー)
  • 列: 各行の属性(email, created_at, plan)
  • 主キー: 行を一意に識別するID
  • リレーション: テーブル同士のつながり(IDで紐づけ)

例: sessions テーブルに user_id があり、それが users の行を指す。これがリレーションです。

要するに、テーブルは「種類」、行は「個体」、リレーションは「関係」です。


PostgreSQLが良いデフォルトである理由

Postgresはデフォルトとして推奨されることが多いです [1]

  • 信頼性: 長年の実績とACIDトランザクション。
  • 汎用性: SaaS、金融、分析、コンテンツなど幅広く対応。
  • スケーラビリティ: 小さく始めて大きく成長可能 [1]
  • 充実したエコシステム: 多くのマネージドサービス [2]
  • オープンソース: ライセンス費用やロックインがない。
  • 互換性: 多くのシステムがPostgres互換 [2]

特別な理由がなければ、Postgresは安全な選択肢です。


Drizzle ORM: TypeScriptの型付きスキーマ

ORM(Object‑Relational Mapper)はSQLを直接書かずにDB操作を可能にします。DrizzleはTypeScriptに最適化されたORMで、SQLに近い書き味と型安全を提供します。

Drizzleが初心者に優しい理由:

  • 単一の真実: スキーマをTypeScriptで定義し、クエリやマイグレーションに再利用 [3]
  • 型安全: 存在しない列はコンパイル時に検出。
  • マジック文字列なし: 関数ベースでクエリを組み立てる。

例: sessions テーブルのスキーマ

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

export const sessions = pgTable(
  "sessions",
  {
    id: varchar({ length: 255 }).primaryKey(),          // Primary key column, a string ID
    user_id: varchar({ length: 255 }).notNull(),        // ID of the user who owns this session (must have a value)
    token: varchar({ length: 512 }).notNull(),          // A unique session token (must have a value)
    expires_at: timestamp({ withTimezone: true }).notNull(), // When the session expires
    ip_address: varchar({ length: 255 }),               // (Optional) IP address of the session
    user_agent: text(),                                 // (Optional) User agent string of the browser/device
    created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
    updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // Indexes and constraints:
    uniqueIndex("sessions_token_unique_idx").on(table.token),  // Ensure no two sessions have the same token
    index("sessions_user_id_idx").on(table.user_id),           // Index to quickly look up sessions by user_id
  ]
);

ざっくり言うと:

  • sessions がテーブル名。
  • iduser_idtokenexpires_at は必須。
  • ip_addressuser_agent は任意。
  • created_atupdated_at は自動入力。
  • token のユニーク制約で重複防止。
  • user_id のインデックスで検索高速化。

TypeScriptのスキーマがそのままマイグレーションの基準になります。


Drizzleでのクエリ(インデックスと安全性)

Drizzleはselect/insert/update/deleteを安全に書けるAPIを提供します。

SQL例:

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

Drizzle例:

import { db } from "@/db";                      // our Drizzle database instance
import { credits as creditsTable } from "@/db/schema";  // the credits table schema we defined

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

安全な理由:

  • 型安全: 列名ミスを事前検知。
  • パラメータ化: SQLインジェクション対策。
  • 意図が読みやすい。

インデックス・制約・安全性:

  • インデックス: 検索を高速化。
  • ユニーク制約: 重複防止。
  • NOT NULL: 必須項目の欠落防止。
  • 型: Postgresが型を強制、Drizzleも型を反映。
  • トランザクション: まとめて成功/失敗。
  • バックアップ: 本番では必須。

ローカル開発と本番の違い

ローカル開発:

  • 自分のPCやDockerで動作。
  • リセットしても安全。
  • .envDATABASE_URL を使う。
  • テストデータのみ。

本番:

  • 実ユーザーデータ。実験は禁止。
  • マネージドDB(Neon、Supabase、RDSなど)。
  • 認証情報の保護、バックアップ、監視が必要。
  • マイグレーションは慎重に [5]

要点: devとprodは分け、マイグレーションは必ずローカルで確認。


よくある質問

Q: SQLは学ぶ必要がありますか?

A: 最初は不要ですが、長期的には役立ちます。ORMでSQLを書かずに進められますが、パフォーマンスやトラブル対応にはSQLが強力です [4]

学ぶと良い基礎:

  • データモデリング
  • SELECT/INSERT/UPDATE/DELETE
  • JOIN
  • 集計(COUNT/SUM)

DrizzleはSQLを隠しません。ORMは補助輪と考えると良いです。

Q: 列やテーブルを変更したら?(マイグレーション)

コードのスキーマ変更は、DBに反映されません。マイグレーションで同期します。

Drizzleの流れ:

  1. TypeScriptのスキーマを変更。
  2. drizzle-kit generate でSQLを生成 [5]
  3. 内容確認(リネーム検出など)。
  4. drizzle-kit migrate で適用。
  5. マイグレーションファイルをコミット。

例: creditsINT から BIGINT に変えるにはマイグレーションが必要。prodではバックアップ必須。


次のステップ: マイグレーションを試す

READMEの指示に従って生成・適用しましょう。例:

npm run db:generate   # generates a migration based on schema differences
npm run db:migrate    # applies the migration to the database

次に:

  • テストデータを挿入。
  • Drizzleで取得。
  • スキーマに列を追加し再マイグレーション。

TypeScriptのスキーマがSQLになり、DBと一致する流れが理解できます。


References