html5中,新增的本地存储的方案有Web Storage、Web Sql Database API、Indexed Database API。
功能上:
Web Storage是一个基于key/value的简单API。value只能存储字符串类型的数据。无法应对复杂的web应用场景。
Web Sql和IndexedDB,都可以存储结构化数据。支持事务、索引。可以应对较为复杂的离线应用场景。
其中Web Sql的操作基于SQL语言。实现了Web Sql的浏览器,底层都是选用Sqlite作为SQL引擎。使用SQL的可以让开发人员从数据存储结构的设计上解脱出来,应用SQL丰富的语法特性,可以直接进行复杂的数据库操作。
IndexedDB的操作直接基于javascript。应用较为低级的API。可以直接透过索引、游标操作数据库中的数据。
标准化和实现:
Web Storage已经被W3C标准化,并被绝大多数现代浏览器实现。
Web Sql API曾经一度作为W3C的工作草案。但之后被废弃。W3C给出的理由是:
This document was on the W3C Recommendation track but specification work has stopped. The specification reached an impasse: all interested implementors have used the same SQL backend (Sqlite), but we need multiple independent implementations to proceed along a standardisation path.
3. Indexed Database API
3.1 概念
3.1.1 数据库
一个数据库的源,跟文档或者worker的源是相同的。每个源都有相关联的数据库。
设置
document.domain
不会影响数据库的源。
每个源都有相关联的数据库。一个数据库由一个或者多个对象存储空间组成,对象存储空间保存着数据库中存储的数据。
在指定的源中,每个数据库都有一个名字来唯一标识。名字可以是任何字符串,包括空串,并且在数据库的存续期间保持约束。每个数据库都有一个当前的版本号. 在数据库被第一次创建时, 它的版本号是0。
实现必须支持所有名字字符串。如果一个实现所采用的存储机制不能处理任意的数据库名字, 那么该实现必须使用转义或者类似的的方式映射到另外一个可以处理的名字。
在删除阶段,数据库有一个删除挂起的标记位。当一个数据将要被删除时,标记位会被设置成true并且所有打开数据库的尝试都会阻塞直到数据库被删除。
打开数据库的操作会创建一个连接。在任意给定的时刻,可能有多个连接建立到给定的数据库。每个连接都有一个关闭挂起的标记,默认值是false。
当一个连接刚建立时,它处于打开状态。连接可以通过几种方式关闭。 如果连接被垃圾回收或者建立连接的执行上下文被销毁(例如用户离开当前页),连接就会被关闭。连接也可以通过关闭一个数据库连接的步骤被显示地关闭。当链接被关闭时删除挂起标记会被设置为真。
IDBDatabase
表示一个数据库上的一个连接。
3.1.2 对象存储空间
对象存储空间是数据库中存储数据的主要存储方式。每个数据库都有一个对象存储空间集合。对象存储空间集合可以被改变,但只可以通过"versionchange"
事务, 例如在一个upgradeneeded
事件当中。当数据库刚被创建时,不存在任何对象存储空间。
对象存储空间中有一个记录表保存着对象存储空间中的数据。每条记录有一个键和值。线性表是按照键升序排列的。在给定的对象存储空间中永远不会存在多条记录有相同键的情况。
每个对象存储空间都有一个名字。名字在所属的数据库中是唯一的。每个对象存储空间可以有一个可选的键生成器和一个可选的键路径。如果对象存储空间有键路径,那么就说它使用内联键。否则就说它使用外联键。
对象存储空间的键可以有三种来源:
IDBObjectStore
接口表示一个对象存储空间。表示同一个对象存储空间的多个实例可以同时存在。
3.1.3 键
为了能够高效地从索引数据库中获取记录, 每条record都会根据它的键进行组织。如果说一个值是一个有效键则它必须遵循ECMAScript[ECMA-262]的类型: Number
原始类型, String
原始类型, Date
对象, 或者 Array
对象。一个Array
要满足有效键的条件,则必须数组中的每个元素都是已定义并且满足有效键的条件(例如稀疏数组不是有效键),并且Array 不能直接或者间接地引用自身。Array
上的非数字属性都会被忽略,但这不会影响这个Array是否是有效键。如果值是Number
类型,则它必须不是NaN才可以是有效键。如果值是Date
类型,它的内部属性,由[ECMA-262]定义的[[原始值]]不能是NaN
。遵循规范的用户代理必须支持所有有效键作为键。
无穷Number
数值是有效键。空Array
也是。
需要接受键作为输入参数的操作必须使每个键参数的值,按顺序地通过结构化克隆算法[HTML5]复制,然后把复制的结果输入,再进行之后的操作。
这个暗含的复制步骤确保了键值在操作后不会因为ECMAScript [ECMA-262]的getters, setters和类型转换方法包括toString()
和 valueOf()
而发生改变。
为了方便比较, 所有的Array
比所有的String
,Date
和Number
都要大; 所有的String
比所有的Date
和Number
大;所有的Date
比所有的Number
大。Number
的值按照数字顺序比较.Date
的值按照时间顺序比较。String
通过章节11.8.5第四步描述的算法(The Abstract Relational Comparison Algorithm, of the ECMAScript Language Specification [ECMA-262])进行比较。Array
通过下列方式比较:
- 让A成为第一个
Array
,让B成为第二个Array
。 - 让变量length为A和B的长度中的较小者。
- 让变量i是0。
- 如果A的第i个元素小于B的第i个元素, 则A小于B。 跳过剩余的步骤。
- 如果A的第i个元素大于B的第i个元素, 则A大于B。跳过剩余的步骤。
- i自增1。
- 如果i不等于length,回到步骤4。否则继续执行下一步。
- 如果A的长度小于B的长度,ength,则A小于B。如果A的长度大于B的长度, 则A大于B。否则A和B相等。
Array
中包含其他Array
是可以的。上述算法对Array中的每个元素都会递归地进行。
单词大于,小于 and 等于 已经在上述的比较中定义。
下面的例子举例说明了当使用内联键和键生成器来存储对象到对象存储空间时的不同行为。
如果下列的条件为真:
那么键生成器生成的数值就会用来填入键值。 在下面的例子中对象存储空间的键路径是 "foo.bar"
。而实际的对象中bar属性上并没有定义值 { foo: {} }
。 当对象存储到对象存储空间时,bar
属性就会被设置为4,因为这是对象存储空间应该生成的下一个键。
"foo.bar" { foo: {} }
如果以下的条件为真:
那么跟键路径关联的属性值就会被使用。自动生成的键不会被使用。 在下面的例子中对象存储空间的键路径是"foo.bar"
。真正对象上的bar
属性的值是10, { foo: { bar: 10} }
。 当对象存储到对象存储空间时bar
属性的值仍为10,因为这就是它的键值。
"foo.bar" { foo: { bar: 10 } }
下面的例子描述了这么一个场景。当指定的内联键通过键路径定义,但没有对应的属性与之匹配。那么键生成器提供的值就会被用来填入键值,并且由系统负责创建对象的层级关系链。在下面例子中对象存储空间的键路径是"foo.bar.baz"
。真正的对象上并没有foo
属性,{ zip: {} }
。当对象存储到对象存储空间时。foo
, bar
, 和baz
属性被依次分别地创建为前一个属性的子对象,直到foo.bar.baz
可以被设置为止。foo.bar.baz
就是对象存储空间的下一个键值。
"foo.bar.baz" { zip: {} }
试图在原始值上设置属性会失败并抛出错误。在下面的第一个例子中对象存储空间的键路径是"foo"
。实际的对象是4
这个原始值。尝试在这个原始值上定义一个属性会失败。对数组来说也一样,数组上不能定义属性。 在下面的第二个例子中, 实际的对象是一个数组, [10]
. 尝试在其上定义一个属性也会失败。
// 键生成器会尝试创建并存储键路径对应的属性到这个原始值上。 "foo" 4 // 键生成器会尝试创建并存储键路径对应的属性到这个数组上。 "foo" [10]
3.1.4 值
每条记录都会关联一个值。遵循规范的用户代理必须支持任意ECMAScript [ECMA-262] 的值,并被结构化克隆算法[HTML5]支持。这支持简单类型比如 String
原始值和 Date
对象 也支持 Object
和 Array
实例, File
对象, Blob
对象, ImageData
对象等等。记录的值通过值传递的方式存储和获取,而不是通过引用传递的方式; 之后对值的改变不会影响存储在数据库中的记录。
3.1.5 键路径
键路径 是一个 DOMString
或者 sequence<DOMString>
定义了如何从值中提取出键。有效的键路径 是下列的一种:
- 空的
DOMString。
- 一个标识符,符合ECMAScript Language Specification [ECMA-262]制定的标识符名称的DOMString
- 一个由两个或者多个标识符组成的由点号分隔的
DOMString
(ASCII码是46的字符)。 - 一个非空的
sequence<DOMString>
只包含符合以上要求的DOMString
。
键路径中不允许空格。
求键路径的值, 参照 使用键路径从值中提取键的步骤。
键路径的值只可以被通过结构化克隆算法明确复制的属性访问。下列的属性也可以:
Blob.size
Blob.type
File.name
File.lastModifiedDate
Array.length
String.length
3.1.6 索引
有时比起通过键去获取对象存储空间中的记录还有更好的方式。 索引 允许 通过对象存储空间的记录的值中的属性来查询对象存储空间的某条记录。
索引是特殊化的持久化键-值存储方式,它有一个引用 的对象存储空间。索引有一个保存着索引中数据的记录表。当引用的对象存储空间中有记录被插入时,索引中记录项是被自动填充的。当引用的对象存储空间有插入、更新或者删除操作时,索引中的记录也是同步自动更新的。同一个对象存储空间可以被若干个索引所引用,这个对象存储空间的变动会触发所有这些索引的更新。
索引中记录的值同样是索引引用的对象存储空间的键的值。键是通过键路径从引用的对象存储空间的值上得到的。如果一条给定记录有键X在索引中有值A,对索引的键路径在A上取值得到Y,那么索引会包含一条记录,它的键是Y,值是X。
索引中记录可以称之为有一个引用值。这是索引所引用的对象存储空间记录的值,该记录的键等于索引中记录的值。 因此在上面的例子中,索引中的键Y,值是X 的记录有一个引用值A。
"Alice"
和值
123
,会有一个
引用值
{ first: "Alice", last: "Smith" }
。
索引中的每条记录会并且只会引用索引引用的的对象存储空间中的一条记录。然而一个索引中可以有多条记录引用对象存储空间中的同一条记录,也可以在索引中找不到跟对象存储空间中给定记录相对应的记录。
索引中的记录总是按照记录的键排序的。然而不像对象存储空间一样,一个给定的索引可以有相同键的多条记录。这样的记录又会按照索引的记录的值排序。(意思是对象存储空间中记录的主键)。
索引都有一个 名字。名字在引用的的对象存储空间中是唯一的。
每个索引页都有一个 unique 标记位。当这个标记位是真的时候,索引会强制任意两条记录都不能有相同的键。如果索引引用的对象存储空间中的一条记录试图插入或修改,从而导致在新的值上对索引的键路径取值的结果已经在索引中已经存在,那么试图的修改动作会产生失败。
每个所以也都有一个 multiEntry 标记位。这个标记位会影响当对索引的键路径取值的结果是一个数组
时的行为。如果multiEntry 标记位置为假,那么一条键是一个数组的记录就会被添加到索引中。如果multiEntry 标记位置为真,那么数组中的每个条目都会被添加到索引中作为一条记录。每条记录的键就是对应数组中每个条目的值。
IDBIndex
接口提供了访问索引的元数据的方法。注意表示同一个索引的这些接口多个实例是可以存在的。
3.1.7 事务
事务是被用来跟数据库中的数据相互作用的。当数据被读取或写入的时候,他是使用事务来完成的。
所有的事务都是通过一个连接建立的,这就是事务的连接。事务有一个 mode(模式)决定了哪种作用类型会被应用到事务上。 mode是在事务创建的时候指定的,在整个事务的生命周期内都维持不变。事务也有一个scope(范围) ,它界定了哪些对象存储空间可能会参与事务。事务有一个active 标记位,它决定了是否新的requests(请求)可以被发起。最后,事务都包含一个requests的请求列表 。
每个事务都有一个固定的作用域,决定了事务何时被创建。事务的作用域在整个事务的生命周期内保持不变。
事务提供了一些对应用和系统错误的保护。一个事务可能被用来存储多条数据记录或者有条件地修改特定的数据记录。一个事务表示了一个原子性的和持续的一系列数据访问操作以及数据变更操作。
事务预期是短暂存续的。这被下面所述的原子性提交特性所支持。开发者依然可以让事务执行较长时间。然而,我们不建议使用这种方式,因为它可能导致糟糕的用户体验。
下面描述了事务的生命周期:
- 事务是通过
IDBDatabase.transaction
创建 的。传入的参数决定了事务的范围以及事务是否是只读的,当事务创建时,它的 active 标记位初始为true。 - 实现必须允许在事务中设置requests,只要它的active标记位是true。即使事务还没有开启的时候也是一样的。直到事务开启之前,实现不可以执行这些请求。然而,实现必须记录requests和他们的顺序。请求只能在事务处于活跃状态时进行设置。当试图对一个处于非活跃状态的事务设置一个请求时,实现必须拒绝并抛出一个
TransactionInactiveError
类型的DOMException异常。 - 一旦实现能按照下面的定义来约束给定的事务模式,实现必须建立一个队列来异步地启动这个事务。发生的时间点受以下的影响:
- 一旦事务开启,实现可以开始执行事务中的requests。除非另外有定义,请求必须按照它们设置的顺序依次执行。同样地,执行的结果也必须按照设置它们的顺序依次返回。不会保证不同事务间请求的返回次序。类似地,事务的模式应当保证不同事务中的两个请求可以以任意的次序执行,而不会影响最终的数据库存储的数据的结果。
- 事务可以在它结束以前,在任意时刻被终止,即使事务不处于活跃状态或还没有开启。当时事务被终止时,实现必须取消(回滚)事务期间对数据库的所有变动。这包括所有对象存储空间的变动和对对象存储空间和索引的增加和删除。
- 事务可以由于非特定
IDBRequest
之外的原因而失败。例如由于提交事务时产生的输入输出错误,或者达到的存储配额上限而导致实现不能继续执行一个请求。在这种情况下实现必须执行事务退出步骤,使用当前的事务对象作为transaction,适当的错误类型对象作为error。例如如果存储配额达到上限,那么QuotaExceededError应当被作为error抛出,如果输入输出错误发生,UnknownError就应当被作为error抛出。 - 当事务不能继续处于活跃状态,只要事务还没有被终止,实现必须试图提交它。这通常发生在所有设置的请求都被执行并且他们的返回都被处理之后,并且没有新的请求再被设置的情况下。当事务被提交时,实现必须原子性地写入所有操作产生的数据库变更。也就是说,要么所有的变更必须都被写入,要么发生了错误,比如磁盘写入错误,实现就不可以写入任何数据库变更。如果这种错误发生,实现必须通过事务退出步骤来终止事务,否则它就必须按照事务提交步骤来提交。
- 当事务被提交或者终止,它就结束了。如果事务不能完成,比如因为实现发生了崩溃,或者由于用户明确地取消了事务,实现必须终止这个事务。
事务可以用下三种模式之一开启。模式决定了并发的对象存储空间访问之间是如何被隔离的。
enum IDBTransactionMode {
"readonly",
"readwrite",
"versionchange"
};
在"readonly"
模式下打开的任意数量的事务允许并发的进行,即使事务的范围是重叠的以及包含了相同的对象存储空间。只要"readonly"
事务正在进行,实现在通过事务创建的requests取得返回数据的时候必须保持原来的数据不变。也就是说,两个读取相同片段数据的请求必须产生相同的返回值,不管是否找到了需要返回的数据结果。
有许多方法可以让实现保证这些。实现可以阻止任何范围重叠的"readwrite"事务,直到 "readonly"
的事务结束。或者实现可以允许 "readonly"
的事务观察一个在事务开启时对象存储空间的快照内容。
相似地,实现必须确保一个"readwrite"的事务只能受到事务自身产生的变动影响。例如,实现必须确保另外一个事务不会修改当前事务范围中的数据。实现必须也确保如果"readwrite"的事务成功完成,提交事务的变更到数据库不需要合并冲突。实现不可以由于合并冲突而终止事务。
如果多个"readwrite"
事务试图访问相同的对象存储空间(比如他们有重叠的范围),首先创建的事务必须优先访问对象存储空间。由于上一段中的要求,这也意味着该事务是在它结束之前,唯一能够访问这个对象存储空间的事务。
一般来讲,上述的要求表示"readwrite"
的事务,如果有重叠的范围的话,就总是按照他们创建的次序执行,而不会并行进行。
"versionchange"
事务从不会跟其他事务并发的进行。当数据库打开时版本号高于当前,新的"versionchange"
事务就会被自动创建,并且可以在upgradeneeded
事件中得到事务对象。upgradeneeded
事件不触发,"versionchange"
事务就不会开启,直到所有其他的到同一个数据库的连接关闭。这保证了所有其他的事务已经结束。
只要"versionchange"
事务正在进行,所有试图打开更多到同一个数据库的连接动作将会被延迟,并且任何试图使用同一个连接去开启额外的事务都会导致抛出异常。这样"versionchange"
事务不仅保证了没有其他事务并发地进行,也保证了只要事务正在进行,就没有其他事务会进入同一个数据库的等待队列。
"versionchange"
事务是当提供的数据库版本号大于当前的数据库版本号时被自动创建的。这个事务将会在upgradeneeded
时间处理函数中处于活跃状态,并且允许创建新的对象存储空间和索引。
用户代理必须确保公平合理的事务级别来避免产生死等。例如,如果多个"readonly"
事务一个接一个地开启,实现不可以无限期地阻塞"readwrite"
事务开启。
事务对象实现自IDBTransaction
接口。
3.1.8 请求
每个对数据库的读写操作都是通过request(请求)。每个请求代表一次读或写操作。Requests有一个done标志位,初始值是false,并且有一个source对象。每个请求也有一个result和一个error属性,他们都是不能访问的,知道done标志位被设置为true。
最后,请求有一个请求事务。 当请求被创建时,它总是通过异步执行请求步骤设置为一个事务对象。步骤设置请求事务为那个事务对象。步骤不会设置请求事务属于那个请求,因为请求会从IDBFactory.open
方法作为返回值返回。但是那个方法创建的请求有一个null的请求事务。
enum IDBRequestReadyState {
"pending",
"done"
};
3.1.9 键范围
记录可以通过键或者键范围的方式从对象存储空间中获取值。键范围 是对应键的数据类型上的一块连续的区间。一个键范围可能被限定下界或上界(有一个值分别对应比它还大或还小的所有元素)。 一个键范围如果同时限定了下界和上界,就说它是有界的。一个键范围既没有限定下界,也没有限定上界,就说它是无界的。一个键范围可能是打开的(键范围不包括它的终点)或者是闭合的(键范围包含它的终点)。键范围可能由单一数值组成。
IDBKeyRange
接口定义了键范围。
interface IDBKeyRange {
readonly attribute any lower;
readonly attribute any upper;
readonly attribute boolean lowerOpen;
readonly attribute boolean upperOpen;
static IDBKeyRange
only (any value);
static IDBKeyRange
lowerBound (any lower, optional boolean open);
static IDBKeyRange
upperBound (any upper, optional boolean open);
static IDBKeyRange
bound (any lower, any upper, optional boolean lowerOpen, optional boolean upperOpen);
};
- 键范围的下界。
- 如果 键范围的下界是闭合的返回false,反之返回true。
- 键范围的上界。
- 果 键范围的上界是闭合的返回false,反之返回true。
lower
any类型,只读
lowerOpen
boolean类型,只读
upper
any类型,只读
upperOpen
boolean类型,只读
-
创建并返回一个新的
键范围,
下界设置为
lower,
下界打开设置为
lowerOpen,
上界 设置为
upper,
上界打开设置为
upperOpen。
如果lower参数或者upper参数不是有效键,或者下界大于上界,或者上下界相同并且都是打开的情况下,实现必须抛出
DataError
类型的DOMException
异常。返回类型:IDBKeyRange
-
创建并返回一个新的
键范围,
下界设置为
lower,
下界打开设置为
open,upper为
undefined,upperOpen为true。如果
lower参数
不是
有效键,实现
必须抛出
DataError
类型的DOMException
异常。返回类型:IDBKeyRange
-
创建并返回一个新的
键范围,
lower和
upper都设置为
value。并且
lowerOpen和
upperOpen都设置为false。如果
upper参数
不是
有效键,实现
必须抛出
DataError
类型的DOMException
异常。 -
返回类型:
IDBKeyRange
-
创建并返回一个新的键范围,下界设置为undefined,下界打开设置为true,upper为upper,upperOpen为open。如果upper参数不是有效键,实现必须抛出
DataError
类型的DOMException
异常。返回类型:IDBKeyRange
bound
, static
lowerBound
, static
only
, static
upperBound
, static
- 键范围的
lower
值是undefined
或者小于键。它也可以等于键 。如果lowerOpen
是false
的话。 - 键范围的
upper
值是undefined
或者大于键。它也可以等于键 。如果upperOpen
是false
的话。
3.1.10 游标
游标是一个在数据库中迭代多条数据的透明机制。存储操作时在相应的索引或者对象存储空间上执行的。游标 包含了索引或者对象存储空间中的一段范围的记录。游标有一个源(source)表明了哪个索引或者对象存储空间是跟这些在游标中迭代的记录相关联的。游标维护了序列中的位置, 其方向是按照记录的键单调递增或者单调递减的顺序。游标也有一对键和值,代表了最近迭代的记录的键和值。游标最后有一个got value标志位。当这个标志位为false时,游标可能在加载下一个值得过程中或者到达了范围的末尾。当是true时,表明游标目前已经有值并且已经准备就绪,可以迭代下一条记录。
游标的方向有四种可能的取值。游标的方向取决了游标的初始位置是位于源的开始处还是结尾处。它也决定了遍历时,游标移动的方向,或者遇到重复值是是否跳过。游标的方向允许的取值如下:
enum IDBCursorDirection {
"next",
"nextunique",
"prev",
"prevunique"
};
如果游标的源是一个对象存储空间,那么游标的有效对象存储空间 就是那个对象存储空间,游标的有效键是游标的位置。如果游标的源是一个索引,游标的有效对象存储空间就是那个索引所引用的对象存储空间并且有效键是游标的对象存储空间位置。
在整个游标范围被迭代之前更改游标所遍历的记录集是允许的。为了保证能处理这个,游标不是用索引维护了他们的位置,而是用前一个返回记录的键。对于一个向前迭代的游标,下一次游标会被要求迭代下一条记录并返回大于之前返回的键的最小者。对于向后迭代的游标,情况则是相反,它会返回小于之前返回的键的最大者。
对于游标迭代索引,则情况就有点复杂因为多条记录会有相同的键,因此也会按照值排序。当迭代索引时,游标也有一个对象存储空间位置,,它代表了索引中之前发现的记录的值。位置和对象存储空间位置都用来查找下一个合适的记录。
游标对象实现了IDBCursor
接口。只能有一个IDBCursor
实例代表一个给定的游标。然而同一时刻可以打开的游标数量是不受限制的。
3.1.11 异常
每个indexedDB规范中定义的异常是一个有指定类型(type)字段的DOMException
类型。[DOM4] 既有的DOM Level 4异常将会设置它们的code为一个遗留的值;然而,新的indexedDB类型异常的code值为0。message的值是可选的。
IndexedDB使用如下的新的DOMException
类型和各种各样的信息字段。所有这些新的类型的code值为0
。
IndexedDB重用了下列的既有DOMException[DOM4]类型。这些类型将会继续按照DOM4规范返回code和name字段;然而,当indexedDB API抛出异常时,它们有如下的信息字段:
3.1.12 配置对象
Options objects are dictionary objects [WEBIDL] which are used to supply optional parameters to some indexedDB functions like createObjectStore
andcreateIndex
. The attributes on the object correspond to optional parameters on the function called.
The following WebIDL defines IDBObjectStoreParameters dictionary type.
dictionary IDBObjectStoreParameters {
(DOMString or sequence<DOMString>)? keyPath = null;
boolean autoIncrement = false;
};
autoIncrement
of type
boolean, defaulting to
false
keyPath
of type
(DOMString or sequence<DOMString>), nullable, defaulting to
null
The following WebIDL defines IDBIndexParameters dictionary type.
dictionary IDBIndexParameters {
boolean unique = false;
boolean multiEntry = false;
};
multiEntry
of type
boolean, defaulting to
false
unique
of type
boolean, defaulting to
false
The following WebIDL defines IDBVersionChangeEventInit dictionary type.
dictionary IDBVersionChangeEventInit : EventInit {
unsigned long long oldVersion = 0;
unsigned long long? newVersion = null;
};
newVersion
of type
unsigned long long, nullable, defaulting to
null
oldVersion
of type
unsigned long long, defaulting to
0
3.1.13 Key Generators
When a object store is created it can be specified to use a key generator. A key generator keeps an internal current number. The current number is always a positive integer. Whenever the key generator is used to generate a new key, the generator's current number is returned and then incremented to prepare for the next time a new key is needed. Implementations must use the following rules for generating numbers when a key generator is used.
- Every object store that uses key generators use a separate generator. I.e. interacting with one object store never affects the key generator of any other object store.
- The current number of a key generator is always set to
1
when the object store for that key generator is first created. - When a key generator is used to generate a new key for a object store, the key generator's current number is used as the new key value and then the key generator's current number is increased by
1
. -
When a record is stored and an key value is specified in the call to store the record, if the specified key value is a
Number
greater than or equal to the key generator's current number, then the key generator's current number is set to the smallest integer number greater than the explicit key. A key can be specified both for object stores which use in-line keys, by setting the property on the stored value which the object store's key path points to, and for object stores which use out-of-line keys, by passing a key argument to the call to store the record.Only specified keys values which are
Number
values affect the current number of the key generator.Date
s andArray
s which containNumber
s do not affect thecurrent number of the key generator. Nor doDOMString
values which could be parsed as numbers. NegativeNumber
s do not affect the current number since they are always lower than the current number. - Modifying a key generator's current number is considered part of a database operation. This means that if the operation fails and the operation is reverted, the current number is reverted to the value it had before the operation started. This applies both to modifications that happen due to thecurrent number getting increased by
1
when the key generator is used, and to modifications that happen due to a record being stored with a key value specified in the call to store the record. - Likewise, if a transaction is aborted, the current number of the key generator's for all object stores in the transaction's scope is reverted to the values they had before the transaction was started.
- When the current number of a key generator reaches above the value
2^53
(9007199254740992
) any attempts to use the key generator to generate a new key will result in aConstraintError
. It is still possible to insert records into the object store by specifying an explicit key, however the only way to use a key generator again for the object store is to delete the object store and create a new one.NOTEAs long as key generators are used in a normal fashion this will not be a problem. If you generate a new key 1000 times per second day and night, you won't run into this limit for over 285000 years.
- The current number for a key generator never decreases, other than as a result of database operations being reverted. Deleting a record from an object store never affects the object store's key generator. Even clearing all records from an object store, for example using the
clear()
function, does not affect the current number of the object store's key generator.
A practical result of this is that the first key generated for an object store is always 1
(unless a higher numeric key is inserted first) and the key generated for an object store is always a positive integer higher than the highest numeric key in the store. The same key is never generated twice for the same object store unless a transaction is rolled back.
Each object store gets its own key generator:
store1 = db.createObjectStore("store1", { autoIncrement: true }); store1.put("a"); // Will get key 1 store2 = db.createObjectStore("store2", { autoIncrement: true }); store2.put("a"); // Will get key 1 store1.put("b"); // Will get key 2 store2.put("b"); // Will get key 2
If an insertion fails due to constraint violations or IO error, the key generator is not updated.
transaction.onerror = function(e) { e.preventDefault() }; store = db.createObjectStore("store1", { autoIncrement: true }); index = store.createIndex("index1", "ix", { unique: true }); store.put({ ix: "a"}); // Will get key 1 store.put({ ix: "a"}); // Will fail store.put({ ix: "b"}); // Will get key 2
Removing items from an objectStore never affects the key generator. Including when .clear() is called.
store = db.createObjectStore("store1", { autoIncrement: true }); store.put("a"); // Will get key 1 store.delete(1); store.put("b"); // Will get key 2 store.clear(); store.put("c"); // Will get key 3 store.delete(IDBKeyRange.lowerBound(0)); store.put("d"); // Will get key 4
Inserting an item with an explicit key affects the key generator if, and only if, the key is numeric and higher than the last generated key.
store = db.createObjectStore("store1", { autoIncrement: true }); store.put("a"); // Will get key 1 store.put("b", 3); // Will use key 3 store.put("c"); // Will get key 4 store.put("d", -10); // Will use key -10 store.put("e"); // Will get key 5 store.put("f", 6.00001); // Will use key 6.0001 store.put("g"); // Will get key 7 store.put("f", 8.9999); // Will use key 8.9999 store.put("g"); // Will get key 9 store.put("h", "foo"); // Will use key "foo" store.put("i"); // Will get key 10 store.put("j", [1000]); // Will use key [1000] store.put("k"); // Will get key 11 // All of these would behave the same if the objectStore used a keyPath and the explicit key was passed inline in the object
Aborting a transaction rolls back any increases to the key generator which happened during the transaction. This is to make all rollbacks consistent since rollbacks that happen due to crash never has a chance to commit the increased key generator value.
db.createObjectStore("store", { autoIncrement: true }); trans1 = db.transaction(["store"], "readwrite"); store_t1 = trans1.objectStore("store"); store_t1.put("a"); // Will get key 1 store_t1.put("b"); // Will get key 2 trans1.abort(); trans2 = db.transaction(["store"], "readwrite"); store_t2 = trans2.objectStore("store"); store_t2.put("c"); // Will get key 1 store_t2.put("d"); // Will get key 2