☰
Lovefield 数据库生命周期全解析:从连接初始化、服务注册到查询执行的设计文档深度解读
2026/10/7 9:38:32 网站建设 项目流程
  • 关系型数据库
  • 数据库
  • 前端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/lov/lovefield
点击查看免费下载

本指南以 Lovefield 设计文档 docs/dd/03_life_of_db.md 为骨架,逐节拆解一个 Lovefield 数据库从“会话全局对象注册”到“数据库初始化(IndexedDB / Firebase / 预取)”,再到“查询上下文构建、参数绑定与计划执行”的完整生命周期。阅读本文后,你将掌握connect()内部到底发生了什么、各连接级服务如何注册与协作、IndexedDB/Firebase 两种后端初始化的差异,以及lf.bind参数绑定机制的真实调用链,并能在实际项目中据此定位初始化与查询阶段的各类问题。

Lovefield 是一个运行在浏览器里的关系型数据库,其整个生命周期被严谨地划分为“数据库的生命周期”与“查询的生命周期”两大部分:前者解决“如何打开、升级、初始化一个数据库实例”,后者解决“一条查询从构建到执行的完整路径”。设计文档 03 节给出了这一过程的总体视图,本文结合仓库源码(lib/global.js、lib/schema/builder.js、lib/base.js、lib/proc/database.js、lib/backstore/indexed_db.js、lib/cache/prefetcher.js 等)逐层印证其实现细节。

1. 全局对象:lf.Global与会话级服务注册

设计文档 3.1 节首先定义了 Lovefield 的全局对象模型:Lovefield 会在全局命名空间(即浏览器window)上注册一个lf.Global.instance_对象,该实例在整个会话内唯一,并被所有连接共享。它的作用是一个服务注册表(service registry):每个数据库连接把自己的连接级组件(缓存、后端存储、查询引擎等)注册进去,后续所有模块通过该注册表按ServiceId取用。

1.1 会话唯一实例与连接级命名空间

源码层面,lib/global.js 用模块级静态字段lf.Global.instance_实现了单例:

lf.Global.instance_; // 会话内唯一的全局实例 lf.Global.get = function() { if (!lf.Global.instance_) { lf.Global.instance_ = new lf.Global(); } return lf.Global.instance_; };

而“每个连接注册自己的 global 对象”这一设计,在 lib/schema/builder.js 的getGlobal()中落地:它以'ns_' + schema.name()作为lf.service.ServiceId,从会话单例中取出(或首次创建)一个按数据库名命名的命名空间 Global,从而实现同一会话内多个数据库连接的服务隔离:

var namespacedGlobalId = new lf.service.ServiceId('ns_' + this.schema_.name()); var global = lf.Global.get(); // 已存在则复用,否则新建并注册 if (!global.isRegistered(namespacedGlobalId)) { namespacedGlobal = new lf.Global(); global.registerService(namespacedGlobalId, namespacedGlobal); }

因为 Lovefield 假设同一会话中一个数据存储上的数据库实例只有一个连接,所以该连接对应的 global 对象同样唯一(多连接写并发的风险与规避策略见 docs/spec/03_life_of_db.md,官方建议用后台页面 / WebWorker / ServiceWorker 统一处理数据库操作)。

1.2 连接级服务清单

设计文档 3.1.1 节指出:支撑查询引擎需要若干**连接唯一(connection-unique)**的组件,它们集中定义在 lib/service.js。从源码看,这些服务通过lf.service.ServiceId模板类注册,预置的服务 ID 包括:

ServiceId 常量服务 ID 字符串注册对象含义
lf.service.BACK_STOREbackstorelf.BackStore实现(IndexedDB / Memory / Firebase / WebSQL / ObservableStore)底层数据存储
lf.service.CACHEcachelf.cache.DefaultCache行缓存(row id → row)
lf.service.INDEX_STOREindexstorelf.index.MemoryIndexStore索引存储
lf.service.QUERY_ENGINEenginelf.proc.DefaultQueryEngine查询计划生成引擎
lf.service.RUNNERrunnerlf.proc.Runner事务/任务执行器
lf.service.OBSERVER_REGISTRYobserverregistrylf.ObserverRegistry查询观察者注册表
lf.service.SCHEMAschemalf.schema.Database已定型的数据库 Schema

设计文档特别强调“大部分服务是在数据库初始化期间创建并注册的”,这正是 lib/base.js 中lf.base.init()的职责,我们将在下一节完整展开。

2. 数据库初始化(Database Initialization)

设计文档 3.2 节指出:Lovefield 中“连接(connect)”意味着在库与数据存储上的数据库之间建立关联,与网络连通性无关。连接通过lf.schema.Builder#connect()或 SPAC 生成的<namespace>.connect()完成,其总体流程为:

  1. 初始化 Global 对象并注册 Schema;
  2. 初始化数据库:
    • 创建并注册行缓存(row cache)与数据存储对象(data store object);
    • 初始化数据存储对象,必要时执行升级(upgrade);
    • 服务初始化(service initialization);
    • 预取数据(prefetch data)。

2.1connect()的入口与约束

lib/schema/builder.js 中connect()首先做连接状态校验:若connectInProgress_为真,或db_已存在且isOpen()为真,则抛出lf.Exception(113)(“Attempt to connect() to an already connected/connecting database”),防止重复连接:

lf.schema.Builder.prototype.connect = function(opt_options) { if (this.connectInProgress_ || (this.db_ !== null && this.db_.isOpen())) { throw new lf.Exception(113); } this.connectInProgress_ = true; if (this.db_ === null) { var global = this.getGlobal(); if (!global.isRegistered(lf.service.SCHEMA)) { global.registerService(lf.service.SCHEMA, this.getSchema()); } this.db_ = new lf.proc.Database(global); } return this.db_.init(opt_options).then(...); };

随后lf.proc.Database#init()(lib/proc/database.js)重新注册 SCHEMA 服务(应对close()后 SCHEMA 被清除的场景),并委托lf.base.init(this.global_, opt_options)完成真正的初始化,成功后将连接标记为活跃。

connect()只接受一个可选的纯 JSON 对象,用于定制连接行为,字段定义见 docs/spec/03_life_of_db.md:

属性类型含义
onUpgradefunction(!lf.raw.BackStore):!IThenable数据库升级逻辑回调
storeTypelf.schema.DataStoreType指定使用的数据存储类型
firebaseFirebaseFirebase 实例(仅storeType = FIREBASE时必须提供)

其中storeType支持INDEXED_DB(默认)、MEMORY(纯内存、onUpgrade必须为undefined)、FIREBASE(必须同时提供已连接认证的 Firebase 实例)以及已废弃的WEB_SQL。若未显式指定,lib/base.js 按“浏览器支持 IndexedDB → 支持 WebSQL(已废弃)→ 纯内存”的优先级自动回退选择:

dataStoreType = capability.indexedDb ? lf.schema.DataStoreType.INDEXED_DB : (capability.webSql ? lf.schema.DataStoreType.WEB_SQL : lf.schema.DataStoreType.MEMORY);

2.2lf.base.init():初始化主干调用链

从源码结构看,lib/base.js 的lf.base.init()是初始化阶段的主干,它精确对应设计文档 3.2.1 的“connect 流程”:

  1. 创建并注册行缓存:new lf.cache.DefaultCache(schema)注册到lf.service.CACHE;
  2. 创建并注册数据存储:根据storeType分支构造lf.backstore.IndexedDB / Memory / ObservableStore / WebSql / Firebase,注册到lf.service.BACK_STORE;Firebase 分支同时置observeExternalChanges = true;
  3. 创建并注册索引存储:new lf.index.MemoryIndexStore()注册到lf.service.INDEX_STORE;
  4. 初始化数据存储:调用backStore.init(options['onUpgrade']),版本不匹配时在此触发升级流程;
  5. 服务初始化:依次创建lf.proc.DefaultQueryEngine(QUERY_ENGINE)、lf.proc.Runner(RUNNER)、lf.ObserverRegistry(OBSERVER_REGISTRY),并执行indexStore.init(schema)(扫描 Schema 中全部索引、创建空索引实例);
  6. 后置处理与预取:Firebase 分支启动lf.backstore.ExternalChangeObserver;若enableInspector为真则暴露全局#lfInspect(供 Lovefield Inspector DevTools 扩展使用);最后创建lf.cache.Prefetcher并执行prefetcher.init(schema)预取全部表数据。

2.3 IndexedDB 初始化与行 ID 扫描

设计文档 3.2.2 节说明:IndexedDB 需要 Schema 中提供的数据库名和版本号来打开连接,版本不匹配时会触发onupgradeneeded或onerror事件。源码 lib/backstore/indexed_db.js 与之对应:

  • 依次探测window.indexedDB / mozIndexedDB / webkitIndexedDB / msIndexedDB,全部缺失则抛lf.Exception(352)(“IndexedDB is not supported by platform”);
  • indexedDB.open(schema.name(), schema.version())打开连接,onupgradeneeded中通过onUpgradeNeeded_()先删除旧索引表(removeIndexTables_,删除名称含.的对象仓库)再创建缺失的表(createTables_),最后调用用户提供的onUpgrade回调;onerror包装为lf.Exception(361);
  • onsuccess中执行scanRowId_():扫描每个表找出最大行 ID,再调用lf.Row.setNextIdIfGreater(rowId + 1)确定本次连接后续可用的行 ID 起点。由于所有 ID 都由 IndexedDB 索引,理论上整表扫描是 O(N),其中 N 是 Schema 中的表数量。

2.4 Firebase 初始化与“版本不匹配”的真实含义

设计文档 3.2.3 节(Firebase Initialization)特别强调:Firebase 后端初始化时会先读取@db/version和@rev/R(变更修订号),版本不匹配时同样调用onUpgrade回调——但此时该名称具有“误导性”:在 Firebase 场景下,版本不匹配通常意味着用户浏览器中运行着缓存的旧版 JS,真正该做的是让用户刷新会话、重新加载更新后的二进制。

源码 lib/backstore/firebase.js 的init()验证了这一点:getValue(this.db_, '@db/version')返回null表示全新数据库(走createNewDb_()+ 调用onUpgrade初始化);与schema.version()相等则读取@rev/R、@table并逐表reloadTable()、initRowId_()、listen_()(监听child_removed与按修订号orderByChild('R').startAt(revision_ + 1)的变更);否则进入onUpgrade_()升级分支后重试。

2.5 服务初始化:行缓存、索引存储与“dumb cache”设计

设计文档 3.2.3 节(Service Initialization)列出初始化期间创建的对象实例:缓存(lf.cache.DefaultCache)、查询引擎(lf.proc.DefaultQueryEngine)、事务执行器(lf.proc.Runner)、索引存储(lf.index.MemoryStore)与观察者注册表(lf.ObserverRegistry),与上文 lib/base.js 的注册顺序一一对应。

文档进一步解释了行缓存的设计动机:

  • 行缓存概念上是一个**“row id → row”的大 Map**(这正是 Lovefield 全库行 ID 唯一的原因);
  • 目前缓存是“笨缓存(dumb cache)”:缓存内容是 IndexedDB 已持久化数据的精确副本。这样做的目的是规避 IndexedDB 在大批量 I/O 上的低效(每次cursor.continue()都要触发一次事件,WebKit 在 HP Z620 上单次事件约 57µs,仅触发 10 万行的 onsuccess 事件就需约 5.7 秒——详见 lib/backstore/indexed_db.js 的 Bundle Mode 注释,以及设计文档 docs/dd/02_data_store.md);
  • 把全部行缓存在内存中,可避免从 IndexedDB 加载数据的额外往返,代价是内存占用。

索引存储方面,当前 Lovefield 仅有内存索引存储(in-memory index store):indexStore.init(schema)会扫描 Schema 中声明的所有索引并创建空索引实例,供后续预取与查询阶段填充使用。

2.6 数据预取(Prefetch Data)与persistentIndex

设计文档 3.2.4 节指出,预取器lf.cache.Prefetcher负责把数据存储中的数据预取进缓存,所有表数据都会被加载到缓存中。源码 lib/cache/prefetcher.js 的实现是一次只加载一张表、严格顺序执行:

lf.cache.Prefetcher.prototype.init = function(schema) { var tables = schema.tables(); var execSequentially = function() { if (tables.length == 0) return goog.Promise.resolve(); var table = tables.shift(); var whenTableFetched = table.persistentIndex() ? this.fetchTableWithPersistentIndices_(table) : // 从存储反序列化索引 this.fetchTable_(table); // 当场重建索引 return whenTableFetched.then(execSequentially); }.bind(this); return execSequentially(); };

关键差异在于表 Schema 是否带persistentIndexpragma:

  • 不带persistentIndex(默认):fetchTable_()通过只读事务读取整表 →cache_.setMany()写入缓存 →reconstructNonPersistentIndices_()遍历每行、按row.keyOfIndex(index.getName())取键并index.add(key, row.id())现场重建所有索引;
  • 带persistentIndex:走fetchTableWithPersistentIndices_(),索引数据从数据存储中反序列化恢复。

设计文档明确指出:默认不持久化索引数据,是为了提升写入速度(持久化索引的维护成本更高),该配置未来可能随更多实际数据反馈而调整。同时 Lovefield 明确承认“初始化期间全量批量加载”这一设计权衡对大数据集不友好,未来计划实现基于MRU 的惰性加载缓存(MRU-based lazy-load cache),在后台按需加载数据。

2.7 Firebase 预取的特殊处理

设计文档 3.2.5 节提醒:对 Firebase 后端而言,预取数据会在数据库初始化阶段真实触发 Firebase 通过网络加载数据(因为 Firebase 数据不在本地)。通常这不是问题——Firebase.js 可能已经持有这些数据;但若数据量很大,就需要同时调优业务代码与 Firebase 服务端设置(如安全规则、分片)来克服初始化期间的网络加载开销。

3. 查询的生命周期(Life of Query)

数据库初始化完成后即可接受查询。设计文档 3.3 节把一条查询的生命划分为四个阶段:

  1. 构建查询上下文(Build query context)
  2. (可选)为参数化查询绑定值(Bind values)
  3. 创建查询计划(Create query plan)
  4. 执行查询计划(Execute query plan)

配合文档目录中的 docs/dd/images/life_of_a_query.png 流程图(用户查询 → Query Parser → Query Engine → Query Executor → 查询答案),可以直观看到查询从输入到结果的完整流转。

3.1 构建查询上下文(Build Query Context)

查询上下文由查询构建器(lf.query.*)构建。设计文档说明:所有查询构建器继承公共基类lf.query.BaseBuilder,并实现查询构建器接口之一(lf.query.Select / Insert / Update / Delete)。从源码结构与文档描述可归纳构建器承担三项主要任务:

  1. 创建查询上下文:如 lib/query/update_builder.js 所示,UpdateBuilder构造时即创建new lf.query.UpdateContext(...)并设置目标表;
  2. 校验输入与语法:构建器各链式方法在写入上下文前校验列存在性、类型与值合法性;
  3. 为参数化查询绑定值(见下节)。

上下文构建成功后,构建器通过exec()(或explain())方法生成查询计划并执行。值得补充的是,lib/proc/database.js 中db.select() / insert() / insertOrReplace() / update() / delete()是构建器对象的工厂入口,它们都先经过checkActive_()校验连接状态(未连接则抛lf.Exception(2))。

3.2 参数绑定(Parameter Binding)

设计文档 3.3.2 节把参数化查询类比为 Oracle / SQLite 的参数化查询 API:在查询上下文中放置占位符(placeholder),运行时用实际值替换(即“绑定”)。参数化查询有两种使用场景:

  • 搜索条件(search condition):通过值谓词(value predicate)实现;
  • 更新集(update set):值保存在UpdateBuilder内部。

搜索条件绑定的机制链如下:lf.bind(index)返回一个lf.Binder对象(见 lib/bind.js,Binder内部仅保存绑定位置索引index_)。当值谓词以单个lf.Binder(大多数运算符)或lf.Binder数组(IN/BETWEEN场景)构造时,谓词内部会持有该 binder 引用;调用bind方法时更新内部存储的value;调用eval方法时返回已绑定的值,若尚未绑定则抛出异常。相关实现见 lib/pred/value_predicate.js。

更新集绑定则由 lib/query/update_builder.js 的set(column, value)完成:它把每个 set 项记录为{ binding: value instanceof lf.Binder ? value.getIndex() : -1, column, value }——即检测value是否为Binder,是则记录绑定位置,否则记为 -1,从而把运行时绑定与静态值统一进同一数据结构。

3.3 创建查询计划与执行查询计划

设计文档将后两个阶段分别指向独立章节:

  • 创建查询计划是 Query Engine 的主要职责:查询引擎接收查询上下文,经过逻辑计划生成、重写与物理计划生成,产出可执行计划;
  • 执行查询计划发生在事务上下文中(隐式事务由exec()触发、显式事务由createTransaction()创建,等价于 SQL 的BEGIN TRANSACTION),详见 Transaction。

4. 生命周期相关的进阶主题

原文档将数据库升级(onUpgrade)、lf.Database接口、多进程连接、导入导出等细节委托给规范文档 docs/spec/03_life_of_db.md,此处补充几点与初始化流程直接相关的要点,帮助读者串联完整生命周期:

  • 升级流程:版本不匹配时 Lovefield 先创建 Schema 中新增的表,再调用用户提供的onUpgrade(rawDb);升级涉及删除/转换表数据时必须自定义升级函数,函数须返回 Promise(promise 被拒绝则connect()一并拒绝)。升级期间对持久化索引的处理是“先全部删除再重建”以保证数据一致性,相关辅助函数(dropTable、addTableColumn、dropTableColumn、renameTableColumn、dump等)的接口定义见 lib/raw.js。
  • lf.Database接口:连接成功后返回的lf.proc.Database实例(lib/proc/database.js)提供getSchema()、select/insert/insertOrReplace/update/delete构建器、createTransaction()、observe()/unobserve()、export()/import()与close()。其中import()必须在空数据库上执行且要求数据对象同名同版本,导入期间不做数据完整性检查(除主键/唯一键外约束关闭);export()/import()都会锁定数据库直至完成。
  • 关闭与删除:close()会复位数据库实例并调用lf.base.closeDatabase()(lib/base.js)关闭后端存储,但受 IndexedDB 限制,不保证close()后再connect()仍只有一个连接;Lovefield 本身不支持删除数据库,如需删除须直接使用 IndexedDB API。

5. 小结与排查建议

综合设计文档与源码,Lovefield 的生命周期可以概括为一张“初始化流水线”+“查询流水线”的组合:

  • 初始化流水线:Builder#connect()→proc.Database#init()→lf.base.init()(注册 CACHE/BACK_STORE/INDEX_STORE →backStore.init(onUpgrade)→ 注册 QUERY_ENGINE/RUNNER/OBSERVER_REGISTRY →indexStore.init()→ 外部变更监听 →Prefetcher.init()全表预取)。
  • 查询流水线:构建器生成上下文 → (可选)lf.bind参数绑定 → 查询引擎生成计划(docs/dd/04_query_engine.md)→ 在事务中执行(docs/dd/05_transaction.md)。

据此可在实际项目中快速定位问题:初始化阶段报错(如 113 重复连接、352 平台不支持、361 无法打开库),优先检查connect()调用次数、浏览器 IndexedDB 支持与 Schema 版本一致性;大表初始化缓慢,根源通常是 lib/cache/prefetcher.js 的逐表全量预取设计;查询阶段报“未绑定参数”,则应沿 lib/pred/value_predicate.js 与 lib/query/update_builder.js 的 binder 持有链检查bind()是否在exec()前完成。

  • 关系型数据库
  • 数据库
  • 前端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/lov/lovefield
点击查看免费下载
上一篇:BentoCloud 待机实例(Standby Instances)配置指南:为 BYOC 集群预置资源、应对流量尖峰
下一篇:创维盒子刷机教程:从安卓到Armbian系统的完美转换

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询