数据访问综合实践
数据访问综合实践 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
零件都齐了:服务端读、Server Action 写、缓存失效、表单反馈。本课做总装——把「个人知识编辑流程」完整串起来:读出笔记、编辑、保存、缓存更新、撤销、刷新后一切如旧。目标不是新知识,是验证组合之后的行为一致。
问题场景
你在笔记页改了一段话,保存,逛完一圈别的课程再回来——改动没了。或者想撤销,却发现「上一个版本」根本没存过。单看每层都没错:读有、写有、缓存也有——错在流程没串紧。综合实践就是把整条链路当产品来验收。
核心解释
一次可靠的编辑流程 = 读带版本、写带校验、失效带路径、撤销带快照。逐段落地。
读:服务端组件查库,拿到带 revision 的当前版本:
// lib/queries/notes.ts
import { getDb } from "@/lib/db/client";
export interface NoteRow {
id: string;
title: string;
body: string;
revision: number;
}
export function getNote(id: string): NoteRow | null {
return (
(getDb()
.prepare("SELECT id, title, body, revision FROM note WHERE id = ?")
.get(id) as NoteRow | undefined) ?? null
);
}
写:快照旧版本与写入新版本放进同一个事务,撤销有据可依:
// app/notes/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { getDb, nowIso, withTransaction } from "@/lib/db/client";
const EditSchema = z.object({
id: z.string().min(1),
title: z.string().min(1).max(200),
body: z.string().min(1).max(50000),
});
export async function updateNote(formData: FormData) {
const parsed = EditSchema.safeParse({
id: formData.get("id"),
title: formData.get("title"),
body: formData.get("body"),
});
if (!parsed.success) throw new Error("参数不合法");
const { id, title, body } = parsed.data;
withTransaction(getDb(), () => {
const prev = getNote(id);
if (!prev) throw new Error("笔记不存在");
// 快照当前版本,撤销才有源头
getDb()
.prepare(
"INSERT INTO note_history (note_id, title, body, revision, saved_at) VALUES (?, ?, ?, ?, ?)"
)
.run(prev.id, prev.title, prev.body, prev.revision, nowIso());
getDb()
.prepare(
"UPDATE note SET title = ?, body = ?, revision = revision + 1, updated_at = ? WHERE id = ?"
)
.run(title, body, nowIso(), id);
});
revalidatePath("/notes");
revalidatePath(`/notes/${id}`);
}
流程一致性靠动作对齐:
- 读:页面调
getNote,服务端渲染。 - 写:快照 + 更新在同一个事务里,要么都成、要么都不成。
- 缓存更新:列表页与详情页都 revalidate,任何入口看到的都是刚写的版本。
- 撤销:从 note_history 取最近快照写回——它本身就是一次 updateNote 式的写,走同样的校验和失效。
- 刷新恢复:数据在 SQLite,刷新只是重新读库;写成功则 F5 后必然一致,不一致说明某层在发旧缓存。
示例拆解
本机验收流程:
- 打开
/notes/[id],改标题保存——当前页立即显示新标题。 - 导航到
/notes列表——新标题同步出现(revalidatePath 生效)。 - 点「撤销上一版」——标题回到旧值,revision 继续增长而不是倒退(撤销也是一次变更)。
- Ctrl+Shift+R 硬刷新——内容与撤销后的状态一致。
关键点:撤销不是「反向写」,而是「再写一次旧快照」。所有变更都经过同一条写路径,note_history 追加式增长,排查问题时它就是时间线。
动手任务
在本机工程里完成:
- 补出 note_history 表与上述 Action,跑通「编辑 → 撤销」闭环。
- 从两个入口(列表点进、直接输 URL)分别验证保存后读到新值。
- 保存后立即硬刷新,确认内容不回跳旧版本;若回跳,用 Network 面板定位是哪一层缓存。
常见错误
- 撤销与更新走了两套代码:一套忘了 revalidate 或校验,行为悄悄分叉;撤销必须复用写路径。
- 快照和更新不在一个事务里:中途失败留下「有历史没更新」或反之的脏状态。
- 只 revalidate 当前页:列表页还躺着旧标题,看起来像「保存失败」。
- 未保存草稿只放在组件 state 里:刷新即丢且无提示;草稿要么落库(草稿表),要么离开页面时警告。
检查与复盘
自查清单:
- 编辑、撤销、刷新之后,列表与详情两个入口看到的数据一致
- note_history 里能找到每次变更前的快照
- 说不一致时知道排查顺序:先查缓存层,再查数据库
参考资料
- Next.js 文档:Updating Data —— Action 写入与 revalidate 的组合
- Next.js 文档:revalidatePath —— 失效范围控制
- Next.js 文档:Server and Client Components —— 读写组件的边界
读完这一课了?
标记完成后,工作台会安排接下来的学习。