danci 2:创建的单词书到底存在哪里?从 Supabase 一路理解 ORM、Drizzle 和 RLS

后台页面已经可以创建单词书了。

但点击"保存"以后,真正的问题才刚开始:

这本单词书到底存在哪里?

这会一路把 PostgreSQL、Supabase、ORM、Drizzle、schema、migration、外键、级联删除和 RLS 全部串起来。


一、先试试最简单的方法:放进数组里

假设管理员创建:

text 复制代码
四级词汇

最简单可以写:

ts 复制代码
const books = []

books.push({
  id: 1,
  name: "四级词汇",
})

页面确实可以显示:

text 复制代码
四级词汇

但是刷新页面、服务器重启以后怎么办?

如果数据只存在运行时内存里:

text 复制代码
程序结束
↓
数据丢失

而真实系统需要长期保存:

text 复制代码
单词书
单词
管理员
用户
学习记录

所以必须引入:

数据库。


二、为什么选择 PostgreSQL

danci 使用 PostgreSQL。

PostgreSQL 是一种关系型数据库。

关系型数据库最基本的数据结构就是:

text 复制代码
Table   表
Row     行
Column  列

例如:

text 复制代码
books

可以是一张表:

id name description
1 四级词汇 CET-4 核心词汇
2 考研词汇 考研英语词汇

这里:

text 复制代码
books

是一张表。

text 复制代码
id
name
description

是字段。

而:

text 复制代码
1 | 四级词汇 | CET-4 核心词汇

是一行记录。

于是创建单词书这件事就变成:

text 复制代码
管理员提交表单
↓
向 books 表插入一行数据

三、数据库有了,难道还要自己部署 PostgreSQL?

当然可以。

但如果自己部署数据库,就需要处理:

text 复制代码
服务器
PostgreSQL 安装
连接配置
网络
账号权限
备份
安全
升级
运维

对于一个正在快速开发的应用来说,这些事情会带来额外成本。

于是 danci 使用:

text 复制代码
Supabase

四、Supabase 到底是什么

Supabase 经常被称为:

text 复制代码
BaaS
Backend as a Service
后端即服务

它提供的核心能力之一就是:

text 复制代码
托管 PostgreSQL 数据库

除此之外还有:

text 复制代码
Auth
Storage
Realtime
Data API
RLS
Postgres Extensions

所以常说:

text 复制代码
使用 Supabase 数据库

没有问题。

但更准确的技术关系是:

text 复制代码
Supabase
└── PostgreSQL

真正负责关系型数据存储的仍然是 PostgreSQL。

Supabase 解决的是:

不必从零搭建整套数据库基础设施,就可以直接获得一个可用的云端 PostgreSQL,以及围绕它的一系列后端能力。


五、那"支持向量数据库"是什么意思

Supabase 的 PostgreSQL 可以启用 pgvector 扩展。

它可以在 PostgreSQL 中存储:

text 复制代码
embedding
向量

并进行:

text 复制代码
向量相似度搜索

例如以后想做:

text 复制代码
根据语义寻找相似单词
根据单词解释做相似检索
RAG / AI 检索

就可能使用向量能力。

但这里需要分清:

danci 当前最核心的数据仍然是普通关系型数据。

例如:

text 复制代码
books
words
users
user_word_records

pgvector 是 PostgreSQL 的扩展能力,不是做普通单词 CRUD 的前提。


六、Supabase 数据库已经在云端,Next.js 怎么连接它?

现在数据库地址在云端。

Next.js 项目需要知道:

text 复制代码
数据库在哪里?
使用什么账号连接?
连接哪个数据库?

这类连接信息通常通过:

text 复制代码
DATABASE_URL

保存。

例如:

env 复制代码
DATABASE_URL=postgresql://...

一般放进:

text 复制代码
.env

于是:

text 复制代码
Next.js
↓
DATABASE_URL
↓
Supabase PostgreSQL

连接起来。

注意:

DATABASE_URL 包含敏感的数据库连接信息,不应该暴露到浏览器端。


七、现在已经能连数据库了,直接写 SQL 不行吗?

当然可以。

查询:

sql 复制代码
SELECT * FROM books;

插入:

sql 复制代码
INSERT INTO books (name)
VALUES ('四级词汇');

修改:

sql 复制代码
UPDATE books
SET name = '大学英语四级'
WHERE id = 1;

删除:

sql 复制代码
DELETE FROM books
WHERE id = 1;

SQL 完全没有问题。

所以 ORM 出现的原因绝不是:

"SQL 不能用。"

真正的问题是:

text 复制代码
项目主要写 TypeScript
↓
数据库使用 SQL
↓
两边需要组织在一起

八、ORM 为什么叫"对象关系映射"

ORM 全称:

text 复制代码
Object Relational Mapping
对象关系映射

先看两个世界。

JavaScript / TypeScript 中:

ts 复制代码
const user = {
  id: 1,
  name: "张三",
}

这是一个:

text 复制代码
Object
对象

数据库里:

text 复制代码
users 表

id | name
---|-----
1  | 张三

这是关系型数据。

实际上两边表达的是同一份业务数据:

text 复制代码
程序世界                    数据库世界

User 对象        ↔          users 表的一行

user.id          ↔          id 字段

user.name        ↔          name 字段

ORM 的核心思想就是:

让程序中的数据结构和关系型数据库中的表、字段、记录建立对应关系,并提供一层数据库操作接口。


九、为什么很多 ORM 示例会使用 user.save()

经典 ORM 中经常可以看到:

ts 复制代码
const user = new User()

user.name = "张三"

await user.save()

程序员操作的是:

text 复制代码
User 对象

而 ORM 最终会转成类似:

sql 复制代码
INSERT INTO users ...

的数据库操作。

因此可以用一张图理解:

text 复制代码
User Object
↓
user.save()
↓
ORM
↓
SQL
↓
PostgreSQL users 表

这非常适合理解"对象关系映射"的思想。

但要注意:

ORM 并不等于一定存在 user.save()

不同 ORM 的 API 风格完全可以不同。


十、Drizzle 为什么看起来和传统 ORM 不太一样

danci 使用:

text 复制代码
Drizzle ORM

Drizzle 的设计非常接近 SQL。

例如:

ts 复制代码
db.select().from(books)

一眼就能想到:

sql 复制代码
SELECT * FROM books;

插入:

ts 复制代码
db.insert(books).values({
  name: "四级词汇",
})

对应:

sql 复制代码
INSERT INTO books ...

所以 Drizzle 并没有试图把 SQL 思维全部藏掉。

它更接近:

text 复制代码
SQL 思维
+
TypeScript API
+
类型安全

因此,"ORM 就是不用懂 SQL"这个理解是不准确的。

更合理的是:

ORM 可以减少重复的底层数据库操作,并把数据库结构更好地纳入应用代码;但表关系、索引、约束、事务、SQL 思维仍然值得理解。


十一、那 Drizzle 怎么知道数据库里有 books 表?

现在 Supabase 已经存在:

text 复制代码
books

但本地 TypeScript 项目怎么知道:

text 复制代码
books 有哪些字段?
id 是什么类型?
name 能不能为空?
哪个字段是主键?

这就需要:

text 复制代码
schema.ts

例如:

ts 复制代码
export const books = pgTable("books", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
})

这段代码是在 TypeScript 中描述:

text 复制代码
books 表
├── id
└── name

于是:

text 复制代码
Drizzle schema              PostgreSQL

books             ↔         books 表

books.id          ↔         id

books.name        ↔         name

建立起联系。


十二、schema.ts 是"建表文件"还是"映射文件"?

这个问题非常关键。

因为实际项目有两种常见路线。

路线一:Code First

先在 TypeScript 中写:

text 复制代码
schema.ts

再让 Drizzle 根据 schema:

text 复制代码
生成迁移
↓
修改数据库

这时:

text 复制代码
代码里的 schema

更接近数据库结构的 source of truth。


路线二:Database First

也可能是:

text 复制代码
Supabase 中已经存在数据库表
↓
再让本地项目获得它的结构

Drizzle Kit 提供 pull,可以从现有数据库 introspect 出 Drizzle schema。

这时候:

text 复制代码
真实数据库
↓
Drizzle schema

数据库更接近结构来源。


所以不能简单记成:

text 复制代码
写一个 schema.ts
=
数据库自动已经有这张表

更准确的是:

schema.ts 是 Drizzle 在代码侧对数据库结构的描述;它究竟是先于数据库还是从数据库同步而来,取决于项目采用 Code First 还是 Database First。


十三、db/index.ts 又做什么?

常见项目可能有:

text 复制代码
db/
├── index.ts
└── schema.ts

职责可以简单分成:

text 复制代码
schema.ts
↓
数据库长什么样

index.ts
↓
怎么连接数据库

例如 index.ts 会:

text 复制代码
读取 DATABASE_URL
↓
建立 PostgreSQL 连接
↓
初始化 Drizzle
↓
得到 db

业务代码最后使用:

ts 复制代码
db.select()
db.insert()
db.update()
db.delete()

十四、数据库字段变了怎么办?

假设最开始:

text 复制代码
books

id
name

后来需求变化,希望增加:

text 复制代码
description

只修改 schema.ts 还不够。

因为真正的 PostgreSQL 表也必须增加:

text 复制代码
description

于是出现:

text 复制代码
Database Migration
数据库迁移

它解决的问题就是:

代码里的数据库结构发生变化以后,真实数据库如何安全地跟着变化。


十五、generate、migrate、push 到底是什么关系

generate

schema.ts 发生变化:

text 复制代码
schema.ts
↓
drizzle-kit generate
↓
生成 SQL migration

它不是"再生成一个 schema 文件"。

它主要根据 schema 的差异生成:

text 复制代码
migration.sql

以及 Drizzle 用来追踪结构变化的快照信息。


migrate

接下来:

text 复制代码
migration.sql
↓
drizzle-kit migrate
↓
执行到数据库

也就是:

把已经生成的迁移真正应用到 PostgreSQL。

所以:

text 复制代码
generate
↓
生成"要怎么改数据库"

migrate
↓
真正执行这些修改

push

还有一种更直接的方式:

text 复制代码
schema.ts
↓
drizzle-kit push
↓
比较 schema 和数据库
↓
直接应用变化

也就是说:

push 可以省略显式生成 migration 文件的步骤,直接把 schema 的变化推到数据库。

开发阶段可能很方便,但团队协作或生产环境通常更重视可追踪的迁移历史。


studio

drizzle-kit studio 用来打开数据库可视化界面。

可以更方便地查看:

text 复制代码
表
字段
数据

十六、一本单词书到底怎么包含很多单词?

数据库连接和 ORM 解决以后,还有更实际的问题:

text 复制代码
四级词汇

里面可能有:

text 复制代码
apple
ability
abandon
...

但是:

text 复制代码
apple

又可能同时属于:

text 复制代码
四级词汇
考研词汇
雅思词汇

因此:

text 复制代码
一本 book 有很多 word
一个 word 也可以属于很多 book

这就是:

text 复制代码
多对多关系

十七、为什么需要 book_words 中间表

多对多关系通常使用中间表:

text 复制代码
books
   │
book_words
   │
words

例如:

book_id word_id
1 10
1 11
2 10

意思是:

text 复制代码
book 1 包含 word 10、11

book 2 也包含 word 10

这样:

text 复制代码
apple

只需要在 words 中保存一次。

它属于哪些单词书,则通过:

text 复制代码
book_words

记录关系。


十八、外键在这里解决什么问题?

例如:

text 复制代码
book_words.book_id

应该指向:

text 复制代码
books.id

数据库可以通过:

text 复制代码
Foreign Key
外键

保证:

text 复制代码
book_words 里的 book_id

不能随便指向一个不存在的 book。

否则就可能出现:

text 复制代码
book_id = 999

但:

text 复制代码
books

里根本没有 id = 999

外键就是关系型数据库维护数据完整性的重要机制之一。


十九、删除一本书后,book_words 怎么办?

假设删除:

text 复制代码
books.id = 1

book_words 里还存在:

text 复制代码
book_id = 1

的关系记录。

这些关系已经没有意义了。

于是可以在外键上设置:

text 复制代码
ON DELETE CASCADE

表示:

text 复制代码
删除 book
↓
自动删除依赖它的 book_words 关系

这就是:

text 复制代码
cascade
级联删除

但它不能无脑使用。

正确判断应该是:

父记录消失以后,这些子记录在业务上是不是也应该必然消失?

如果答案是"是",才适合考虑 cascade。


二十、用户开始背单词以后,又出现了一个安全问题

words 记录:

text 复制代码
apple
苹果
音标
例句

这些数据大家都一样。

但:

text 复制代码
用户 A:apple 已掌握
用户 B:apple 学习中

却不是公共数据。

因此用户学习记录应该单独设计,例如:

text 复制代码
user_word_records

user_id
word_id
status
review_count

这时就出现了:

用户 A 能不能读取用户 B 的学习记录?

当然不能。

这就是 RLS 出现的原因。


二十一、RLS 到底是什么?

RLS:

text 复制代码
Row Level Security
行级安全

重点在:

text 复制代码
Row
行

它不是简单控制:

text 复制代码
能不能访问这张表

而是进一步控制:

text 复制代码
当前用户可以访问这张表里的哪些行

例如:

user_id word_id status
1 10 mastered
1 11 learning
2 10 learning

用户 1 登录后,只能访问:

text 复制代码
user_id = 1

的行。

用户 2:

text 复制代码
user_id = 2

这就实现了:

text 复制代码
同一张表
↓
不同用户看到不同数据

二十二、words 是公共表,所以"不需要开 RLS"吗?

这个说法容易产生误解。

真正要区分的是两个问题:

text 复制代码
这张表的数据是不是公开的?

和:

text 复制代码
这张表是否启用了 RLS?

它们不是同一个概念。

如果 words 通过 Supabase Data API 暴露给客户端,更稳妥的做法是:

text 复制代码
开启 RLS
↓
再明确配置访问策略

例如可以配置成:

text 复制代码
所有用户允许 SELECT words

这代表:

text 复制代码
公开读取

而不是:

text 复制代码
完全关闭行级安全

对于:

text 复制代码
user_word_records

则可以配置:

text 复制代码
只能访问 user_id = 当前登录用户

因此可以理解成:

text 复制代码
words
↓
公开数据
↓
策略允许读取

user_word_records
↓
私有数据
↓
策略只允许访问自己的行

真正重要的是:

根据业务决定访问策略,而不是简单用"公共表 / 私有表"判断要不要安全机制。


二十三、Authentication 和 RLS 解决的也不是一个问题

Authentication 解决:

text 复制代码
当前用户是谁?

例如:

text 复制代码
当前登录用户 id = 123

RLS 解决:

text 复制代码
既然你是 123
↓
数据库里的哪些行允许你访问?

所以:

text 复制代码
Authentication
↓
身份认证

RLS
↓
数据授权

这两个概念要分开。


二十四、把第二篇完整串起来

现在整个数据层终于能连成一条线:

text 复制代码
管理员创建一本单词书
↓
数据必须长期保存
↓
PostgreSQL
↓
不想从零部署数据库基础设施
↓
Supabase
↓
Next.js 要连接数据库
↓
DATABASE_URL
↓
业务代码要操作 PostgreSQL
↓
Drizzle ORM
↓
本地需要描述表结构
↓
schema.ts
↓
结构变化需要同步数据库
↓
migration

再往业务里面走:

text 复制代码
books 和 words 多对多
↓
book_words
↓
Foreign Key
↓
ON DELETE CASCADE

用户开始学习后:

text 复制代码
产生用户私有记录
↓
RLS

这一整条链路就是 danci 数据层真正的骨架。


二十五、但是数据库现在还是空的

到这里:

text 复制代码
books 表有了
words 表也设计好了

可新的问题又出现了:

难道几千个单词要在后台一个一个手工录入?

当然不可能。

所以接下来就进入这个项目里非常有价值的一块:

text 复制代码
GitHub 开源数据
↓
JSON
↓
数据清洗
↓
scripts
↓
AI Coding

这就是第三篇。

相关推荐
东风破_1 小时前
danci 项目(三):从 JSON 数据到 AI Coding,真实项目里的数据清洗、Prompt 和工程规范
前端·后端·node.js
东风破_1 小时前
danci 项目(一):从需求到架构,一个单词学习系统为什么会这样设计
前端·后端·node.js
橙子家2 小时前
OSS 文件上传的几个风险点和解决方案
数据库
jay神3 小时前
【计算机毕业设计】基于SpringBoot的程序教学辅助系统
java·前端·vue.js·spring boot·后端·毕业设计·课程设计
2601_962066493 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
愤怒的苹果ext4 小时前
MySQL Shell备份恢复数据库
数据库·mysql·备份恢复·mysqlsh
2601_962073816 小时前
Spring全面详解(基础版)
java·后端·spring
数字新视界6 小时前
DCIM管理系统的技术架构与部署模式详解
数据库·物联网·数据中心·数据中心基础设施管理·dcim管理系统
何以解忧,唯有..6 小时前
Pydantic 介绍与使用:Python 数据校验的现代方案
数据库·python·microsoft