在这里插入图片描述
在这里插入图片描述

实例:备忘录便签(Memo)|技术:便签字段、置顶/颜色字段、MemoDao

一、业务需求分析:便签的数据形态

便签(Memo)是轻量级记事工具——比日记轻(不用长篇大论)、比任务清单自由(不用状态管理)。它的数据行为有几个独特之处:

  1. 置顶:重要便签钉在列表最前(「待办」钉在顶部提醒自己)——排序字段 pinned
  2. 颜色标签:便签用不同颜色区分用途(黄色=工作、粉色=购物、绿色=健身)——颜色即分类;
  3. 部分更新:改标题不动内容、换颜色不动标题——单字段轻量更新是便签的高频操作;
  4. 多彩墙展示:彩色便签像便利贴一样铺满墙面,视觉即内容。

核心需求清单:

需求 数据层实现 页面呈现
新建便签 insert(标题+内容+颜色) 便签墙新增彩签
置顶/取消 部分更新 pinned 字段 置顶区/普通区
换颜色 部分更新 color 字段 便签变色
编辑 全量更新 弹窗编辑
颜色筛选 按 color 过滤 颜色筛选条
删除 delete 长按删除

二、字段设计:一张灵活的便签表

便签表 memo

字段名 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
title TEXT NOT NULL 标题
content TEXT DEFAULT ‘’ 内容
color TEXT DEFAULT ‘#FFE8A3’ 便签底色 HEX
pinned INTEGER NOT NULL DEFAULT 0 0 普通 / 1 置顶
created_time INTEGER NOT NULL 创建时间戳
updated_time INTEGER NOT NULL 最近修改时间戳

设计要点拆解

1. color 存 HEX 色值字符串。便签颜色是「展示属性」——直接存 #FFE8A3 这样的色值,页面 backgroundColor(c.color) 零转换渲染。为什么不用枚举数字?因为颜色是「开放式取值」(用户可以加新颜色),枚举反而限制;而且色值本身就是可读的。展示型属性存原始值

2. pinned 用 0/1 数字。置顶与否是「状态」,可参与排序(ORDER BY pinned DESC)和过滤(WHERE pinned=1)——数字编码正确选择。排序时置顶在前:orderByDesc('pinned') 让 1(置顶)排 0(普通)前面。

3. updated_time 驱动排序。便签墙的默认顺序:置顶在前,同状态按更新时间倒序(最近编辑的在前)——ORDER BY pinned DESC, updated_time DESC两个排序键的组合让置顶区和普通区内部都「最新的在前」。

4. content 可空。便签可以只写标题(如「超市采购清单」),content 默认空串。标题必填(NOT NULL),内容可选。

5. 六色便签色板。DAO 导出色板常量供页面使用:

export const MEMO_COLORS: string[] = [
  '#FFE8A3', '#FFD1DC', '#C9F2C7', '#BFE3FF', '#E2D5FF', '#FFE0B3',
];

米黄/粉/绿/蓝/紫/橙六色——新建便签时循环取色,颜色筛选条也用这个数组。色板常量单一来源:页面取色、筛选、渲染都引用它。

三、建表 SQL 与索引

CREATE TABLE IF NOT EXISTS memo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT DEFAULT '',
  color TEXT DEFAULT '#FFE8A3',
  pinned INTEGER NOT NULL DEFAULT 0,
  created_time INTEGER NOT NULL,
  updated_time INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memo_pinned ON memo (pinned);

idx_memo_pinned 索引:服务「置顶在前」的排序与「置顶区」过滤。pinned 只有 0/1 两个值,索引收益有限,但表达「这是查询热点」的意图。数据量小(12 张便签)时索引与否无差别——本实例建索引更多是「声明式意图」

四、MemoDao 封装:三种更新形态

数据层核心 MemoDao。本实例的技术亮点是更新方法的三种形态——全量更新、部分更新(置顶)、部分更新(颜色):

形态一:全量更新(编辑内容)

static async update(context: common.Context, m: Memo): Promise<number> {
  const store = await MemoDao.getStore(context);
  const values: relationalStore.ValuesBucket = {
    title: m.title, content: m.content, color: m.color,
    updated_time: m.updatedTime,
  };
  const predicates = new relationalStore.RdbPredicates(MemoDao.TABLE);
  predicates.equalTo('id', m.id);
  return await store.update(values, predicates);
}

编辑弹窗保存时调用——更新标题、内容、颜色三个业务字段 + 刷新 updated_time(不更新 pinned 和 created_time)。

形态二:部分更新(仅置顶状态)

static async togglePin(context: common.Context, id: number, pinned: number): Promise<number> {
  const store = await MemoDao.getStore(context);
  const values: relationalStore.ValuesBucket = {
    pinned: pinned,
    updated_time: Date.now(),
  };
  const predicates = new relationalStore.RdbPredicates(MemoDao.TABLE);
  predicates.equalTo('id', id);
  return await store.update(values, predicates);
}

部分更新的精髓ValuesBucket 只放 pinned(+ 顺带刷新 updated_time)——其他字段(标题、内容、颜色)完全不动。这就是「只改置顶」的轻量操作,避免了「先查全量再更新全量」的冗余读。

形态三:部分更新(仅换颜色)

static async changeColor(context: common.Context, id: number, color: string): Promise<number> {
  const store = await MemoDao.getStore(context);
  const values: relationalStore.ValuesBucket = {
    color: color,
    updated_time: Date.now(),
  };
  const predicates = new relationalStore.RdbPredicates(MemoDao.TABLE);
  predicates.equalTo('id', id);
  return await store.update(values, predicates);
}

与 togglePin 同构——只改 color 字段。两个部分更新方法展示了 RDB 的天然能力UPDATE 语句本来就可以只更新指定列,ValuesBucket 里有什么就更新什么。这是 SQLite 相对「ORM 全量更新」的优势——按需更新,零冗余写

五、查询方法:全量、按颜色、计数

全部便签(置顶在前 + 更新倒序)

static async queryAll(context: common.Context): Promise<Memo[]> {
  const store = await MemoDao.getStore(context);
  const predicates = new relationalStore.RdbPredicates(MemoDao.TABLE);
  predicates.orderByDesc('pinned').orderByDesc('updated_time');
  const result = await store.query(predicates);
  return MemoDao.collect(result);
}

双键排序:先按 pinned 降序(置顶组在前),组内按 updated_time 降序(最近编辑在前)。排序的层级结构:主键定分组、次键定组内顺序。

按颜色筛选

static async queryByColor(context: common.Context, color: string): Promise<Memo[]> {
  const store = await MemoDao.getStore(context);
  const predicates = new relationalStore.RdbPredicates(MemoDao.TABLE);
  predicates.equalTo('color', color).orderByDesc('pinned').orderByDesc('updated_time');
  const result = await store.query(predicates);
  return MemoDao.collect(result);
}

计数(标题栏统计):

static async count(context: common.Context): Promise<number> {
  const store = await MemoDao.getStore(context);
  const result = await store.querySql(`SELECT COUNT(*) AS c FROM ${MemoDao.TABLE}`);
  let total = 0;
  if (result.goToNextRow()) {
    total = result.getLong(result.getColumnIndex('c'));
  }
  result.close();
  return total;
}

六、技术要点对照表

技术点 实现方式 生产价值
置顶排序 orderByDesc(‘pinned’) 重要便签在前
双键排序 pinned + updated_time 分组 + 组内序
部分更新 ValuesBucket 只放目标列 单字段轻量写
颜色存储 HEX 字符串 UI 零转换
色板常量 MEMO_COLORS 导出 单一来源
更新排序 部分更新时刷 updated_time 编辑上浮

七、文章小结

备忘录便签的数据层核心是**「置顶 + 颜色」两个特色字段 + 三种更新形态**:pinned 驱动置顶排序(双键排序的分组结构)、color 驱动彩色墙与颜色筛选、部分更新(togglePin/changeColor)展示 RDB「按需更新」的轻量能力。这是「轻量内容应用」的标准建模——字段少、更新灵、排序活。

下一篇(11-2)展示多彩便签墙 UI——置顶区 + 颜色筛选 + 新建浮钮,像便利贴墙一样赏心悦目。


八、便签表字段设计详解:五个字段背后的决策

建表容易,建好表难。便签表六列虽少,每一列都对应一个产品决策。本节把五个核心字段逐个拆解,讲清「为什么这样设计、换一种设计会怎样」。

8.1 title:必填的「门面字段」

决策点 本实例选择 备选方案 取舍理由
是否必填 NOT NULL 允许 NULL 便签墙卡片靠标题识别,空标题 = 空墙
长度限制 TEXT 不限长 VARCHAR(50) SQLite 的 TEXT 无长度上限,UI 层负责限制输入
空串处理 允许 禁止 极少数场景可只写内容,UI 兜底显示「无标题」

标题是便签墙的「门面」——卡片第一行永远是标题。约束层(NOT NULL)管「必须有值」,UI 层管「值不能是空白」,双保险。

8.2 content:纯文本的可空内容

content 允许为空(DEFAULT ‘’),因为便签可以只写标题。内容按纯文本存储,详见第十节「富文本与纯文本的选择」。换行用 \n 字符直接存进 TEXT——查询、展示、编辑全程零转换。

8.3 color:HEX 字符串——展示属性存原始值

颜色是本实例最有代表性的「展示型属性」:不参与业务计算、只参与渲染。存 HEX 字符串 #FFE8A3 而非枚举数字,三个理由:

  1. 零转换渲染backgroundColor(c.color) 直接用,无需查映射表;
  2. 开放式取值:后续加颜色不改表结构、不改 DAO;
  3. 可读性#FFD1DC 一看就是粉色,数字 2 代表什么要翻文档。

HEX 存储的纪律:色板常量统一大写、六位、不带 alpha(#RRGGBB)。若将来要半透明,扩为八位 #AARRGGBB 即可——存储格式兼容,只是约定升级。色值纪律比约束更重要:数据库管不了大小写,MEMO_COLORS 色板常量就是「事实上的校验」。

8.4 pinned:0/1 状态位——排序与过滤的双用字段

pinned 用 INTEGER 0/1 而非 TEXT ‘yes’/‘no’,因为它是参与排序的状态

用法 SQL 表达 数字方案 文本方案
排序 ORDER BY pinned DESC 1 在前、0 在后 需 CASE WHEN 转数字
过滤 WHERE pinned = 1 直接等值 引号 + 大小写敏感
更新 SET pinned = 1 直接赋值 需防 ‘YES’/‘No’ 混乱

数字 0/1 在排序、过滤、更新三个场景全部「零转换」,文本方案处处要转换或兜底。参与计算的字段用数字编码,参与展示的字段存原始值——这是字段类型选择的黄金法则。

8.5 两个时间戳:created_time 与 updated_time 的分工

字段 写入时机 服务场景
created_time insert 时一次写入 新建时间展示
updated_time insert + 每次更新都刷新 排序 + 最近修改展示

created_time 是「身份证」,一次写入不再变;updated_time 是「心跳」,任何变更(改内容、换颜色、置顶)都要刷新——因为它是排序键之一。两个时间戳不能合并:合并后「创建时间」会随编辑漂移,「最近编辑」会丢失。

九、置顶排序思路:置顶优先 + 时间倒序

便签墙的排序是一条规则:先置顶、后普通;同组内最近编辑在前。实现就是双键 ORDER BY:

SELECT * FROM memo ORDER BY pinned DESC, updated_time DESC;

9.1 为什么不是「独立置顶表」

有人会问:置顶是不是该单独建一张 memo_pin 关联表(memo_id + pin_time)?本实例选择直接在 memo 表加 pinned 字段

维度 单表 pinned 字段 独立置顶表
查询 一条 SQL 双键排序 JOIN 或子查询
更新 UPDATE memo SET pinned=1 插入/删除关联记录
事务 单表原子 两表需事务
适用量级 百张以内 海量数据场景

独立置顶表适合「置顶是长列表独立操作」的场景(如消息列表);便签数量少、置顶操作频繁(toggle),字段方案更简单直接——简单需求用简单建模。

9.2 置顶时刷新 updated_time 的连锁反应

togglePin 部分更新时顺带刷新 updated_time:

const values: relationalStore.ValuesBucket = {
  pinned: pinned,
  updated_time: Date.now(),
};

这一步是有意为之:置顶本身是一次「用户关注的行为」,刷新时间让刚置顶的便签在置顶组内也上浮到最新。排序键与操作绑定——任何改变用户关注度的操作都刷新 updated_time,排序永远反映「最近被关注」。

十、富文本与纯文本:内容字段的技术选型

便签内容存什么格式,是内容字段最重要的选型。本实例选纯文本

维度 纯文本 富文本(HTML/Markdown)
存储 原始字符串 带标签字符串(体积 3~10 倍)
编辑 TextArea 直接读写 需解析器 + 编辑器
渲染 Text 组件直接显示 需 RichText/Web 解析
搜索筛选 无影响 标签干扰 LIKE 匹配
复杂度 零依赖 需引入渲染库

纯文本的优势在于整条数据链路零转换:写入(TextArea.value)→ 存储(TEXT)→ 展示(Text)→ 编辑回填,全程同一字符串。\n 换行符原样存储,卡片预览取第一行即可:

// 卡片摘要:取内容第一行,超 20 字省略
private preview(content: string): string {
  const line = content.split('\n')[0] ?? '';
  return line.length > 20 ? `${line.slice(0, 20)}` : line;
}

何时才需要富文本:内容需格式排版(加粗、列表、链接)、内容超长需折叠展示。便签是短内容 + 彩色卡片视觉,纯文本已够用,多一分存储就多一分复杂度

十一、FAQ:建表与数据层常见问题

Q1:为什么 id 用 AUTOINCREMENT 而不是 UUID?
便签是单机数据,无跨设备合并需求;自增整数索引紧凑、查询快。若未来做多端同步,再迁移为字符串主键。

Q2:置顶便签多了会不会影响查询性能?
pinned 只有 0/1,idx_memo_pinned 索引区分度低(每个值约一半数据),实际走表扫描。但便签表数据量小(百张以内),扫描开销可忽略。索引收益与数据量成正比——本实例建索引更多是表达查询意图。

Q3:颜色存 HEX,用户自定义颜色怎么校验?
数据层只保证非空,格式校验放 UI 层(色板选择 + 正则 ^#[0-9A-Fa-f]{6}$ 兜底)。数据层不做无业务意义的强校验,校验放在产生数据的边界(UI)。

Q4:部分更新不读旧值,会不会覆盖其他字段的并发修改?
单用户单设备无并发写;且 UPDATE 只写 ValuesBucket 中的列,其他列保持原值——这正是部分更新的安全性来源。

Q5:同样置顶的便签顺序会不会乱?
不会。双键排序是确定性的:pinned 相同按 updated_time 倒序,updated_time 也相同则按 id 倒序兜底——排序键越加越稳,生产环境建议显式追加 id 作第三键。

Q6:内容存富文本标签会不会更好?
见第十节。便签场景纯文本是「够用且简单」的最优解;需求进化到排版时,再评估迁移成本。

十二、本章小结

本文补齐了建表与数据层的完整拼图:五列字段各有决策依据(必填/可空/编码/展示)、颜色 HEX 的存储纪律、置顶排序的字段 vs 关联表之争、纯文本的内容选型。建表不是写 SQL,而是把产品决策翻译成列约束——每一列都应该回答「为什么这样存」。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐