18-RowSet三缓冲:primary、filter、delete
Row管一行,RowSet管一组行的生命周期。三个ArrayList——primary存有效行、filter存筛选项、delete存待同步的删除行。remove对INSERT行的特殊处理、collect的脏数据收集、reset的整体重建,这篇把222行拆完。
文章目录
- 18-RowSet三缓冲:primary、filter、delete
-
- 一、三个缓冲的职责
- 二、add:两行代码创建INSERT行
- 三、remove:INSERT行的分岔
- 四、collect:脏数据收集
- 五、accept与reject:提交后的双向清算
- 六、reset:查询结果的整体灌入
- 七、getrow这个命名
源码:browise-data/src/main/java/com/browise/data/RowSet.java(222行)
一、三个缓冲的职责
public class RowSet {
private final List<Row> primary = new ArrayList<>(); // 当前有效行
private final List<Row> filter = new ArrayList<>(); // 筛选结果
private final List<Row> delete = new ArrayList<>(); // 待同步的已删行
}
| primary | NONE/INSERT/UPDATE | add/remove/查询灌入 | 渲染/编辑 |
| filter | 任意 | 本地筛选(客户端过滤) | 渲染筛选视图 |
| delete | DELETE | remove移入 | collect提交 |
filter是三缓冲里最"轻"的——它只是primary某个时刻的视图快照,不参与提交协议。真正的提交协议由 primary+delete 两个缓冲构成。
二、add:两行代码创建INSERT行
public Row add(Map<String, Object> data) {
Row row = new Row(data);
row.setStatus(RowStatus.INSERT); // 新行天生INSERT
primary.add(row);
return row;
}
注意 new Row(data) 构造时状态是NONE(第17篇),add里立刻改成INSERT——“这一行是用户刚创建的,库里没有”。返回row引用让调用方继续填字段:
Row newRow = rowSet.add();
newRow.setItemValue("name", "张三"); // INSERT状态下不记original(第17篇)
三、remove:INSERT行的分岔
public void remove(Row row) {
int idx = primary.indexOf(row);
if (idx >= 0) {
primary.remove(idx);
if (row.getStatus() != RowStatus.INSERT) {
row.setStatus(RowStatus.DELETE);
delete.add(row); // 非INSERT行进delete缓冲
}
// INSERT行:从primary删掉就完了——库里没它,无需同步
}
}
这是三缓冲设计最关键的一行判断:
| INSERT | 从primary移除,不进delete | 新增未入库,删了=从未存在,提交时无需任何SQL |
| NONE/UPDATE | 移除+标DELETE+进delete缓冲 | 库里有这行,提交时要发DELETE |
如果不分岔、INSERT行也进delete——提交时对库里不存在的行发DELETE,Oracle报"0行受影响"(不报错但日志脏);更糟的是和数据库触发器/审计联动时会误判。
removeAt(int)是同一逻辑的索引版——两份代码没有互相复用(一个indexOf先),因为实现都只有8行,抽公共方法的成本大于重复。
四、collect:脏数据收集
public List<Row> collect() {
List<Row> dirty = new ArrayList<>();
for (Row row : primary) {
if (row.isDirty()) dirty.add(row); // INSERT/UPDATE行
}
dirty.addAll(delete); // DELETE行
return dirty;
}
三行核心逻辑——primary里的脏行 + delete缓冲的全部行。这个列表就是提交报文里rows数组的来源(第23篇commonSave按_t分发)。
注意collect不改变任何状态——纯只读操作。收集完行还是脏的,直到accept或reject才清。
前端useRowSet的collect(‘auto’|‘all’|‘dirty’)多一个模式参数——后端只有dirty语义(提交协议只需要脏数据)。'all’是前端导出场景(把整个primary交出去)。
五、accept与reject:提交后的双向清算
(第19篇专门讲,这里先给对照)
public void accept() {
// 存活行全部归零
for (Row row : primary) {
row.setStatus(RowStatus.NONE);
row.getOriginals().clear();
}
delete.clear();
filter.clear();
}
public void reject() {
// ①倒序移除INSERT行
for (int i = primary.size() – 1; i >= 0; i—) {
if (primary.get(i).getStatus() == RowStatus.INSERT) primary.remove(i);
}
// ②UPDATE行从originals整体恢复
for (Row row : primary) {
if (row.getStatus() == RowStatus.UPDATE) {
row.getMap().clear();
row.getMap().putAll(row.getOriginals());
row.getOriginals().clear();
row.setStatus(RowStatus.NONE);
}
}
delete.clear();
filter.clear();
}
一个值得注意的细节:reject恢复UPDATE行用的是getMap().clear()+putAll(originals)整体替换,而不是逐字段original回填。因为originals只记录了改过的字段——整体替换相当于"没改过的字段保留data原值(本来就等于original),改过的字段回到original"。两种写法结果等价,clear+putAll避免了逐字段比较。
还有个隐蔽点——reject后delete里的行丢了!delete.clear()直接清空,被删的行没有回到primary。这是browise与PB DataWindow的行为差异(PB的reject会把delete缓冲的行恢复回primary)——第19篇讲修复历史时展开。
六、reset:查询结果的整体灌入
public void reset(List<Map<String, Object>> rows) {
primary.clear();
filter.clear();
delete.clear();
if (rows != null) {
for (Map<String, Object> m : rows) {
primary.add(new Row(m)); // 新Row状态=NONE
}
}
}
每次查询后全量重建——不是增量merge。旧primary里的脏行、delete缓冲全部丢弃。
为什么不做增量(比如按主键merge保留本地未提交的编辑)?——场景不需要。查询通常是"重新取数",用户点了刷新就是想看最新数据;本地编辑要么先提交要么先放弃。增量merge带来的主键冲突、状态合并复杂度远超收益。KISS在这个数据结构上贯彻得很彻底。
七、getrow这个命名
public Row getrow(int index) { return primary.get(index); }
全小写getrow(不是getRow)——从wiserise的Java代码继承的原名。改名是一行事,但前端useRowSet.ts、所有调用点、以及两代项目共用的心智模型都要跟着动。协议稳定的成本意识贯穿到方法命名——不是不能改,是不值得改。
✅ 亮点:222行RowSet拆成三缓冲职责表、remove对INSERT行不进delete缓冲的关键分岔、collect三行逻辑的提交协议角色、reset全量重建不做增量merge的KISS取舍、getrow命名的协议稳定成本。适合实现行集管理的参考。扩展方向:第19篇accept/reject修复史、第23篇commonSave、第44篇前端useRowSet。


