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がテーブル名。id、user_id、token、expires_atは必須。ip_addressとuser_agentは任意。created_atとupdated_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で動作。
- リセットしても安全。
.envのDATABASE_URLを使う。- テストデータのみ。
本番:
- 実ユーザーデータ。実験は禁止。
- マネージドDB(Neon、Supabase、RDSなど)。
- 認証情報の保護、バックアップ、監視が必要。
- マイグレーションは慎重に [5]。
要点: devとprodは分け、マイグレーションは必ずローカルで確認。
よくある質問
Q: SQLは学ぶ必要がありますか?
A: 最初は不要ですが、長期的には役立ちます。ORMでSQLを書かずに進められますが、パフォーマンスやトラブル対応にはSQLが強力です [4]。
学ぶと良い基礎:
- データモデリング
SELECT/INSERT/UPDATE/DELETEJOIN- 集計(COUNT/SUM)
DrizzleはSQLを隠しません。ORMは補助輪と考えると良いです。
Q: 列やテーブルを変更したら?(マイグレーション)
コードのスキーマ変更は、DBに反映されません。マイグレーションで同期します。
Drizzleの流れ:
- TypeScriptのスキーマを変更。
drizzle-kit generateでSQLを生成 [5]。- 内容確認(リネーム検出など)。
drizzle-kit migrateで適用。- マイグレーションファイルをコミット。
例: credits を INT から 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と一致する流れが理解できます。