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是表名。id、user_id、token、expires_at为必填。ip_address、user_agent为可选。created_at、updated_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 运行。
- 可随意重置。
- 使用
.env的DATABASE_URL。 - 测试数据。
生产环境:
- 真是用户数据,不可试错。
- 通常使用托管 DB(Neon、Supabase、RDS)。
- 需要安全凭证、备份与监控。
- 迁移需谨慎执行 [5]。
要点:dev 与 prod 分离,迁移先在本地验证。
常见问题
Q: 使用 Postgres 和 Drizzle 必须学 SQL 吗?
A: 一开始不必,但长期很有帮助。ORM 可以减少写 SQL,但 SQL 能帮你调优与排错 [4]。
建议掌握:
- 数据建模(表与关系)
- 基本查询:
SELECT、INSERT、UPDATE、DELETE - Joins:连接相关数据
- 聚合:统计、求和等
Drizzle 并不隐藏 SQL,只是更安全。把 ORM 当作“辅助轮”就好。
Q: 如果我改列或表怎么办?(迁移)
代码改了 schema,数据库不会自动同步。迁移用来同步两者。
Drizzle 迁移流程:
- 在 TypeScript 中更新 schema。
- 运行
drizzle-kit generate生成迁移 [5]。 - 检查迁移内容(可能提示 rename)。
- 运行
drizzle-kit migrate应用。 - 提交迁移文件确保环境一致。
示例:把 credits 从 INT 改为 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 与代码一致。