- 关系型数据库
- 数据库
- 前端
【免费下载链接】lovefield
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.
Lovefield 是一个面向 Web 应用的关系型数据库,用 JavaScript 编写、跨浏览器运行,对外提供 SQL 风格的查询 API。本文基于官方规范文档 docs/spec/08_referential_integrity.md,系统讲解 Lovefield 的外键(Foreign Key)约束语义、RESTRICT与CASCADE两种动作模式,以及IMMEDIATE与DEFERRABLE两种约束检查时序;并结合仓库源码(lib/schema、lib/cache)与测试用例(tests/proc)说明这些规则在引擎内部的真实落地方式。读完本文,你将能够在建表时正确声明外键约束、预判各操作在不同模式下的行为,并理解 Lovefield 如何在客户端数据库中以可预测的方式保证引用完整性。
一、外键约束的语义:约束仅在建表期定义
Lovefield 的规范明确:外键约束只能在 schema 创建期间定义,且所有外键约束必须满足以下条件(违反任何一条都会抛出lf.Exception.SYNTAX_ERROR):
- 一个外键约束恰好建立在两个已存在的列之间:父列(parent column,也称 referenced / 被引用列)和子列(child column,也称 referencing / 引用列)。
- 父列和子列可以属于同一张表,也可以属于不同表(即允许自引用外键)。
- 父列和子列必须具有相同且可索引的
lf.Type。 - 父列必须是唯一的:要么是主键,要么被标记为 unique。
- 一个约束的子列不能被声明为另一个外键约束的父列(即不允许外键约束链)。
- 禁止循环引用(cyclic references)。
对应到代码层面,上述约束在 schema 构建阶段由 lib/schema/builder.js 中的三个私有方法在finalize_()时统一校验:
checkForeignKeyValidity_()(lib/schema/builder.js):校验父表存在(错误码 536)、父列存在(错误码 537)、父子列类型一致(错误码 538)、父列唯一(错误码 539);checkForeignKeyChain_()(lib/schema/builder.js):检测外键链,即某个外键的父列恰好是另一个外键的子列(错误码 534);checkFkCycle_():基于 DFS 检测有向图中是否存在环(错误码 533),其算法注释引用了《算法导论》第 22 章关于“有向图无环当且仅当 DFS 不产生回边”的结论。
此外,lib/schema/table_builder.js 中的checkPrimaryKeyNotForeignKey_()还禁止主键列同时作为外键子列(错误码 543),这是建表期另一条与引用完整性相关的硬性校验。
外键的声明语法
外键通过TableBuilder.addForeignKey(name, rawSpec)声明,其中rawSpec的类型定义在 lib/schema/foreign_key_spec.js:
/** * @typedef {{ * local: string, // 子列(child column)列名 * ref: string, // 父列,格式为 "父表名.父列名" * action: (lf.ConstraintAction|undefined), // RESTRICT 或 CASCADE * timing: (lf.ConstraintTiming|undefined) // IMMEDIATE 或 DEFERRABLE * }} */ lf.schema.RawForeignKeySpec;ref必须采用表名.列名的两段式语法,若格式非法(如缺少.),lf.schema.ForeignKeySpec构造函数会抛出错误码 540(lib/schema/foreign_key_spec.js)。一个典型用法如下:
var db = lf.schema.create('demo', 1); db.createTable('Department') .addColumn('id', lf.Type.INTEGER) .addPrimaryKey(['id']) .addColumn('name', lf.Type.STRING); db.createTable('Employee') .addColumn('id', lf.Type.INTEGER) .addPrimaryKey(['id']) .addColumn('departmentId', lf.Type.INTEGER) .addForeignKey('fk_dept', { local: 'departmentId', // 子列 ref: 'Department.id', // 父列("父表名.父列名") action: lf.ConstraintAction.RESTRICT, timing: lf.ConstraintTiming.IMMEDIATE });声明外键时 Lovefield 还会自动为子列建立一个索引(lib/schema/table_builder.js),该索引的规范化名称即约束名(childTable.name),后续运行时约束检查正是依赖这个索引完成的(详见下文第四节)。在 Codelab 与测试的 schema 中也能看到这种声明方式的实际使用,例如 tests/proc/end_to_end_foreign_key_cascade_test.js。
二、约束动作模式:RESTRICT 与 CASCADE
每个外键约束都有两种动作模式,由lf.ConstraintAction枚举表示,定义于 lib/enums.js:
lf.ConstraintAction.RESTRICT = 0; lf.ConstraintAction.CASCADE = 1;若在 schema 定义中未显式指定动作模式,默认值为RESTRICT(由 lib/schema/table_builder.js 中的addForeignKey()补齐默认值)。下面分别说明两种模式的行为。
2.1 RESTRICT 模式
在该模式下,任何违反约束的操作都会被直接取消。规范以"父表 = 被引用表、子表 = 引用表"为基准,给出了三种操作的具体规则:
INSERT:若父表中不存在对应行,则不能向子表插入新行。UPDATE/INSERT_OR_REPLACE:- 若被修改的行在子表中有相关行,则不能更新父表被引用列;
- 若子表引用列在父表中没有对应行,则不能更新子表引用列。
DELETE:若父表的某行在子表中有相关行,则不能删除该行。
在运行时实现中,RESTRICT 检查由 lib/cache/constraint_checker.js 完成,主要分两个方向:
- 检查"被引用的键必须存在"(
checkReferredKeys_(),lib/cache/constraint_checker.js):用于 INSERT 和 UPDATE 场景,验证子列引用的父键确实存在于父索引中,否则抛出错误码 203(Foreign key constraint violation)。注意其中的一个细节:子列为 null 时会被忽略,不视为违例。 - 检查"没有引用者"(
checkReferringKeys_(),lib/cache/constraint_checker.js):用于 UPDATE 和 DELETE 场景,通过 schema 的getReferencingForeignKeys()找出引用当前被修改表的外键,若子索引中仍包含被删除/被修改的父键,则抛出错误码 203。
对应到事务处理流程,Journal会在插入、更新、删除时分别调用checkForeignKeysForInsert()、checkForeignKeysForUpdate()、checkForeignKeysForDelete()(lib/cache/constraint_checker.js),其中更新和删除场景只针对RESTRICT动作执行阻断式检查。
2.2 CASCADE 模式
在该模式下,约束违例不会取消操作,而是自动修改相关表以维持数据完整性:
INSERT/INSERT_OR_REPLACE:与 RESTRICT 模式行为相同(父行必须存在才能插入子行)。需要特别注意的是:insertOrReplace()查询不触发级联;若希望级联效果,应改用update()查询。DELETE:删除父表某行时,子表中所有相关行会被自动删除。UPDATE:更新父表被引用列时,所有引用列会被自动更新为相同的值。
需要强调的一点是:Lovefield 不提供为DELETE和UPDATE分别指定约束模式的粒度——一旦处于CASCADE模式,UPDATE和DELETE操作都会级联。
级联的运行时实现同样位于 lib/cache/constraint_checker.js:
detectCascadeUpdates()(lib/cache/constraint_checker.js):找出所有因父列值变更而需要同步更新的子行,返回"行 ID → {外键约束、原始更新行}"的映射;detectCascadeDeletion()(lib/cache/constraint_checker.js):迭代式地找出所有需要级联删除的行,并记录表处理顺序(tableOrder)以保证删除过程中不破坏任何约束——这也是为什么级联删除能穿透多层引用而无需用户关心删除次序;findReferringRowIds_()(lib/cache/constraint_checker.js):专门查找CASCADE动作下引用被修改行的子表行 ID。
事务层则通过 lib/cache/journal.js 的updateByCascade_()(第 303 行)和removeByCascade_()(第 335 行)把检测结果真正应用到缓存中,完成级联更新与级联删除。
三、约束检查时序:IMMEDIATE 与 DEFERRABLE
在RESTRICT模式下,约束还可以标记为DEFERRABLE或IMMEDIATE,以控制检查发生的时机。在CASCADE模式下,DEFERRABLE的概念不适用,因此统一使用IMMEDIATE。时序参数通过 schema 创建 API 的可选枚举参数暴露,枚举名为lf.ConstraintTiming,定义于 lib/enums.js:
lf.ConstraintTiming.IMMEDIATE = 0; lf.ConstraintTiming.DEFERRABLE = 1;若未提供该参数,默认值为IMMEDIATE(同样由 lib/schema/table_builder.js 补齐)。此外,lib/schema/table_builder.js 中还有一条硬性规则:不允许CASCADE动作与DEFERRABLE时序组合,若同时指定会抛出错误码 506(Lovefield allows only immediate evaluation of cascading constraints)。
3.1 DEFERRABLE(可延迟)
可延迟约束在事务提交之前才被强制检查。在事务生命周期内,各条语句可以违反该约束而不会抛出任何错误——约束检查被推迟到事务即将提交的那一刻统一执行。这为需要"先写子行、后补父行"的批处理场景提供了灵活性。
从源码实现看,checkReferredKeys_与checkReferringKeys_都接收一个constraintTiming参数,只检查timing == constraintTiming的约束(lib/cache/constraint_checker.js),即运行时按时序筛选出本次应检查的约束集合。
3.2 IMMEDIATE(立即)
立即约束在每条语句执行期间就被强制检查。这是默认行为,也是最严格的模式——任何一条 INSERT/UPDATE/DELETE 语句若违反约束,会立刻抛出lf.Exception(错误码 203)并使该操作失败。
关于 DEFERRABLE 约束检查被推迟到"提交前"这一行为,可以参考事务相关的测试与文档,如 tests/proc/end_to_end_foreign_key_test.js 中对外键违例场景的验证,以及 docs/spec/05_transaction.md 对事务生命周期的描述。
四、约束的运行时检查机制:索引驱动
Lovefield 的引用完整性检查完全基于索引完成,而非扫描整张表,这也是其性能保证的关键。在addForeignKey()时,引擎会自动为子列建立索引(lib/schema/table_builder.js):
this.addIndex(name, [spec.childColumn], this.uniqueColumns_.has(spec.childColumn));即约束名直接成为子列索引名。运行时:
ConstraintChecker从全局服务获取IndexStore、Schema和Cache(lib/cache/constraint_checker.js);- 通过
getParentIndex_()(lib/cache/constraint_checker.js)找到父列的索引(父列必为主键或 unique,因此必然存在专属索引),并用缓存 Map 避免重复查找; - 对每条修改记录,先判断被引用列的值是否真的发生了变化(
didColumnValueChange_,lib/cache/constraint_checker.js),仅在值变化时才执行containsKey()/get()等索引查询; - 子列值为 null 时跳过检查,视为"未引用"。
整个流程避免了全表扫描,约束违例检测的时间复杂度取决于索引查询的开销,这也是 Lovefield 能在客户端数据库场景下高频执行 DML 的底气之一。
五、通过测试与 YAML 配置验证行为
仓库为引用完整性提供了充分的测试与数据样本:
- tests/proc/end_to_end_foreign_key_cascade_test.js:在同一 schema 中混合声明
CASCADE与RESTRICT外键(如第 114-137 行所示),分别验证级联删除、级联更新以及 RESTRICT 阻断行为; - tests/proc/end_to_end_foreign_key_test.js:覆盖外键违例、INSERT/UPDATE/DELETE 各方向上的约束检查;
- tests/proc/end_to_end_foreign_key_cascade_test.js 之外,
spac/testdata目录下还有一批专门用于验证非法 schema 定义的 YAML 样本,例如:- foreign_key_invalid_action.yaml
- foreign_key_invalid_combination.yaml
- foreign_key_invalid_local_column.yaml
- foreign_key_invalid_reference.yaml
- foreign_key_invalid_remote_column.yaml
- foreign_key_non_unique.yaml
- foreign_key_chain.yaml 与 foreign_key_loop.yaml
这些样本分别对应前文提到的"无效引用语法(540)、类型不匹配(538)、非唯一父列(539)、外键链(534)、循环引用(533)"等违例场景,由 spac/run_test.js 驱动的 parser/codegen 测试执行校验,可作为理解建表期规则边界的直接参考。
六、实践要点速查
- 外键只能在建表期声明,且
ref必须写成父表名.父列名; - 不指定
action时默认为RESTRICT(阻断违例操作),不指定timing时默认为IMMEDIATE(逐语句检查); RESTRICT+DEFERRABLE可将检查推迟到事务提交前,适合"先写子后写父"的批处理;CASCADE会同时级联 UPDATE 和 DELETE,但不接受DEFERRABLE,且insertOrReplace()不级联——需要级联更新时请改用update();- 约束检查基于索引完成(子列会自动建索引),子列取 null 值时不参与引用检查;
- 若在建表或运行期收到
SYNTAX_ERROR或错误码 203/533/534/536/537/538/539/540/543/506,可对照上文定位到对应的 schema 声明问题或数据违例。
延伸阅读
- 完整规范索引:docs/spec_index.md,本主题对应的规范原文见 docs/spec/08_referential_integrity.md
- Schema 定义细节:docs/spec/01_schema.md 与 docs/dd/01_schema.md
- 事务与生命周期:docs/spec/05_transaction.md、docs/spec/03_life_of_db.md
- 相关源码:外键规格解析 lib/schema/foreign_key_spec.js、建表期校验 lib/schema/table_builder.js 与 lib/schema/builder.js、运行期检查 lib/cache/constraint_checker.js 与 lib/cache/journal.js
- 关系型数据库
- 数据库
- 前端
【免费下载链接】lovefield
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.
相关推荐
深入Austin TUI源码:从控制器到视图的终端UI设计思想
深入Austin TUI源码:从控制器到视图的终端UI设计思想 Austin TUI是一款基于Austin性能分析工具的终端用户界面,采用了经典的MVC(模型
技术深度:BatteryML如何构建企业级电池寿命预测平台
技术深度:BatteryML如何构建企业级电池寿命预测平台 在电动汽车和储能系统快速发展的今天,电池健康状态预测已成为制约行业发展的关键技术瓶颈。传统电池管理系
机器学习科研特征工程数据分析GORM约束管理:外键约束与数据完整性终极指南
GORM约束管理:外键约束与数据完整性终极指南 GORM作为Golang生态中最受欢迎的ORM库,提供了强大的约束管理功能,帮助开发者轻松维护数据库表之间的关系
后端数据库ORM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考