数据库基础:从 Excel 到 Supabase
如果你用过 Excel,你已经理解数据库 90% 的核心概念。唯一的区别是:Excel 的格子你自己点,数据库的格子用代码读写。
Excel 思维 vs 数据库思维
Excel 做数据管理:
| 姓名 | 文章标题 | 点赞数 |
|---|---|---|
| 张三 | 数据库入门 | 12 |
| 李四 | CSS Grid 详解 | 8 |
在 Supabase(PostgreSQL)里,这个表格对应一张"表"(table):
articles 表
─────────────────────────────────
id | title | views | category
─────┼──────────────┼───────┼─────────
1 | 数据库入门 | 12 | 科技前沿
2 | CSS Grid 详解 | 8 | 科技前沿
Excel 有你手动算 SUM 和筛选,数据库用 SQL 查。白物集不直接写 SQL,改用 @supabase/supabase-js(Supabase 的 JavaScript 客户端库),它在背后帮你生成 SQL。
三步:连接、查询、写入
白物集在后端(Express API)连接 Supabase 的代码就三行:
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(supabaseUrl, supabaseKey);
这跟 Excel 打开文件的逻辑一模一样——你得先指定"哪个文件"(supabaseUrl),再提供"打开权限"(supabaseKey)。只不过 Excel 文件存在本地,Supabase 存在云端。
连接之后,查询文章列表:
let query = supabase
.from('articles')
.select('id, title, category, published_at')
.eq('status', 'published')
.order('published_at', { ascending: false })
.range(0, 19);
拆开看,每步都是 Excel 里你手动做的事情:
| 代码 | Excel 对应操作 |
|---|---|
.from('articles') |
切换到「articles」这个工作表 |
.select('id, title, ...') |
选中 A、B、C 三列 |
.eq('status', 'published') |
筛选:状态 = "已发布" |
.order('published_at', descending) |
按发布日期降序排序 |
.range(0, 19) |
只看前 20 行 |
写入数据也一样直观:
const { data, error } = await supabase
.from('articles')
.insert({
title: '数据库基础',
category: '科技前沿',
status: 'published',
})
.select('id, title');
这等于在 Excel 表格的最后一行下面新建一行,填入三个单元格的值,然后 Excel 返回给你新行的内容。
白物集踩坑:不传 content_html,页面显示 "内容加载中"
这是白物集踩过最多次的坑。POST /api/v1/content/ingest 不会自动把 content_md 转成 content_html,而文章详情页的模板逻辑是:
article.content_html || '<p>内容加载中...</p>'
如果只传了 content_md,content_html 为空字符串,页面上就是「内容加载中」。修复办法:POST 前用 Python 的 markdown 库手动转一次:
import markdown
html = markdown.markdown(md_clean, extensions=['fenced_code', 'tables'])
同时 payload 里传 content_md 和 content_html 两个字段。这个坑的本质是:数据库只存你传进去的数据,不会帮你算任何东西。Excel 的公式是自动重算的,数据库不会。
表和表的关系
Excel 可以把 A 工作表的某列引用到 B 工作表的某行。数据库也一样。
白物集的 article_likes 表就关联到 articles 表:
CREATE TABLE article_likes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
article_id BIGINT NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
user_ip TEXT NOT NULL,
UNIQUE(article_id, user_ip)
);
REFERENCES articles(id) 表示:每一行 article_likes 数据都必须指向一条真实存在的文章。这等于在 Excel 里做了「数据验证 → 序列 → 引用另一个工作表」,只是数据库强制执行——你塞不进去一个不存在的文章 ID。
查询关联数据时,先查文章,再查它的点赞数:
// 查文章
const { data: article } = await supabase
.from('articles')
.select('id, title')
.eq('id', articleId)
.single();
// 查点赞数
const { count } = await supabase
.from('article_likes')
.select('*', { count: 'exact', head: true })
.eq('article_id', articleId);
第一次写这段代码时踩了一个性能陷阱:每查一篇文章就单独查一次点赞数。首页列表有 20 篇文章,就要发 21 次请求。后来改成用 select 的关联查询(或者在文章查询时直接返回计数),但为了演示简单的两步查询,上面的分步写法更容易理解。
数据库和 Excel 的本质区别
最后说一个很多人没意识到的区别:Excel 是你一个人用,数据库是服务器用。
你在 Excel 里改了数据,保存,发给别人。数据库不是——前端不直接连数据库,中间隔着一层 API。
白物集的架构是:
用户浏览器 → Astro SSR → Express API → Supabase
前端通过 fetch('https://ai.golfr20.cn/api/articles') 拿到数据,前端代码里不从 Supabase 客户端读取。所以 supabase.ts 中的匿名 key 即使暴露在浏览器端,也只能读公开数据,写操作受到行级安全(RLS,Row Level Security)控制。
这个架构和安全意识,是数据库和 Excel 最根本的分水岭。
下一篇讲白物集 API 架构拆解:19 个路由的分层设计——怎么从一条 Express 路由,长成一个可维护的后端系统。