前置知识

SaaS 数据库入门:Postgres + Drizzle 讲人话

面向初学者的 PostgreSQL 与 Drizzle ORM 指南:表、行、关系、为什么选 Postgres、TypeScript 类型化 schema、查询、索引、数据安全、迁移、以及本地 vs 生产。

SaaS 数据库入门:Postgres + Drizzle 讲人话

做 SaaS 需要存数据:用户、订单、文章、会话等。本指南用 PostgreSQL(Postgres)作为默认选择,讲清数据库基础,并介绍如何用 Drizzle ORM 在 TypeScript 中定义和查询 schema。

你将学到:

  • 表、行、列、关系的含义。
  • 为什么 Postgres 是默认首选。
  • Drizzle 如何用类型化 schema 写查询。
  • 索引与约束如何保持性能和数据质量。
  • 本地开发与生产环境的区别。
  • 迁移(migrations)为什么重要。

表、行、关系(不讲术语)

把数据库表想成一张表格:

  • 表:一类数据(如 users)。
  • 行:一条记录(一个用户)。
  • 列:字段(email、created_at、plan)。
  • 主键:唯一标识一行的 ID。
  • 关系:通过 ID 将两张表关联。

例子:sessions 表里有 user_id 指向 users 的一行。这个链接就是关系,所以 Postgres 是关系型数据库。

一句话:表存类型,行存实例,关系把东西连接起来。


为什么 PostgreSQL 是默认首选

Postgres 常被推荐为默认选择 [1],原因包括:

  • 可靠:多年生产实践与 ACID 事务。
  • 通用:适用于 SaaS、金融、内容等多场景。
  • 可扩展:从小规模成长到大规模 [1]
  • 生态好:大量托管服务与工具 [2]
  • 开源:无厂商锁定、无授权费。
  • 兼容性强:很多系统讲“Postgres” [2]

除非你有非常特殊的需求,否则 Postgres 是稳妥的基础。


Drizzle ORM:TypeScript 类型化 schema

ORM(对象关系映射)让你用代码而不是手写 SQL 操作数据库。Drizzle 是现代 TypeScript ORM,保留 SQL 直观感,同时带来类型安全。

为什么 Drizzle 友好:

  • 单一真相:TypeScript schema 同时用于查询和迁移 [3]
  • 类型安全:字段不存在会在编译期报错。
  • 不用魔法字符串:通过函数构建查询。

示例:sessions 表 schema

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 schema 还能作为迁移的依据。


用 Drizzle 查询(含索引与安全性)

Drizzle 提供 select/insert/update/delete 的流式 API,并生成安全 SQL。

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 注入风险。
  • 语义清晰:代码即查询意图。

索引、约束与安全:

  • 索引:加速频繁查询。
  • 唯一约束:避免重复(如 email)。
  • NOT NULL:防止必填字段为空。
  • 数据类型:Postgres 强制类型,Drizzle 同步类型。
  • 事务:多步操作要么全成功、要么全失败。
  • 备份:生产必须启用。

本地开发 vs 生产环境

本地开发:

  • 在本机或 Docker 运行。
  • 可随意重置。
  • 使用 .envDATABASE_URL
  • 测试数据。

生产环境:

  • 真是用户数据,不可试错。
  • 通常使用托管 DB(Neon、Supabase、RDS)。
  • 需要安全凭证、备份与监控。
  • 迁移需谨慎执行 [5]

要点:dev 与 prod 分离,迁移先在本地验证。


常见问题

Q: 使用 Postgres 和 Drizzle 必须学 SQL 吗?

A: 一开始不必,但长期很有帮助。ORM 可以减少写 SQL,但 SQL 能帮你调优与排错 [4]

建议掌握:

  • 数据建模(表与关系)
  • 基本查询:SELECTINSERTUPDATEDELETE
  • Joins:连接相关数据
  • 聚合:统计、求和等

Drizzle 并不隐藏 SQL,只是更安全。把 ORM 当作“辅助轮”就好。

Q: 如果我改列或表怎么办?(迁移)

代码改了 schema,数据库不会自动同步。迁移用来同步两者。

Drizzle 迁移流程:

  1. 在 TypeScript 中更新 schema。
  2. 运行 drizzle-kit generate 生成迁移 [5]
  3. 检查迁移内容(可能提示 rename)。
  4. 运行 drizzle-kit migrate 应用。
  5. 提交迁移文件确保环境一致。

示例:把 creditsINT 改为 BIGINT 必须迁移。生产环境务必先备份。


下一步:实践 Drizzle 迁移

按照项目 README 运行迁移。示例:

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

然后:

  • 插入测试数据。
  • 用 Drizzle 查询回来。
  • 给 schema 加一列再迁移一次。

这样你会直观理解 TypeScript schema 如何变成 SQL,并保持 DB 与代码一致。


References