ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Lovefield 数据库生命周期全解析:从连接初始化、服务注册到查询执行的设计文档深度解读

Lovefield 数据库生命周期全解析:从连接初始化、服务注册到查询执行的设计文档深度解读 关系型数据库数据库前端【免费下载链接】lovefieldLovefield 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 → rowlf.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()完成其总体流程为初始化 Global 对象并注册 Schema初始化数据库创建并注册行缓存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 流程”创建并注册行缓存new lf.cache.DefaultCache(schema)注册到lf.service.CACHE创建并注册数据存储根据storeType分支构造lf.backstore.IndexedDB / Memory / ObservableStore / WebSql / Firebase注册到lf.service.BACK_STOREFirebase 分支同时置observeExternalChanges true创建并注册索引存储new lf.index.MemoryIndexStore()注册到lf.service.INDEX_STORE初始化数据存储调用backStore.init(options[onUpgrade])版本不匹配时在此触发升级流程服务初始化依次创建lf.proc.DefaultQueryEngineQUERY_ENGINE、lf.proc.RunnerRUNNER、lf.ObserverRegistryOBSERVER_REGISTRY并执行indexStore.init(schema)扫描 Schema 中全部索引、创建空索引实例后置处理与预取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 storeindexStore.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 节把一条查询的生命划分为四个阶段构建查询上下文Build query context可选为参数化查询绑定值Bind values创建查询计划Create query plan执行查询计划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。从源码结构与文档描述可归纳构建器承担三项主要任务创建查询上下文如 lib/query/update_builder.js 所示UpdateBuilder构造时即创建new lf.query.UpdateContext(...)并设置目标表校验输入与语法构建器各链式方法在写入上下文前校验列存在性、类型与值合法性为参数化查询绑定值见下节。上下文构建成功后构建器通过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.jsBinder内部仅保存绑定位置索引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)升级涉及删除/转换表数据时必须自定义升级函数函数须返回 Promisepromise 被拒绝则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()前完成。赞分享关系型数据库数据库前端【免费下载链接】lovefieldLovefield 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点击查看免费下载相关推荐MXNet contrib.io 模块实战用 DataLoaderIter 打通 Gluon DataLoader 与符号式 Module 训练MXNet contrib.io 模块实战用 DataLoaderIter 打通 Gluon DataLoader 与符号式 Module 训练 本文聚焦 A关系型数据库数据库前端如何配置eslint-plugin-simple-import-sort的自定义分组规则如何配置eslint plugin simple import sort的自定义分组规则 eslint plugin simple import sort是一款nuklear轻量级ANSI C GUI库的Go绑定让跨平台界面开发更简单nuklear轻量级ANSI C GUI库的Go绑定让跨平台界面开发更简单 nuklear是一个为轻量级ANSI C GUI库nuklear.h提供Go绑定UI组件上一篇BentoCloud 待机实例Standby Instances配置指南为 BYOC 集群预置资源、应对流量尖峰下一篇创维盒子刷机教程从安卓到Armbian系统的完美转换创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表