此答案中使用的术语复合查询是指在其 WHERE 子句中涉及多个条件的 SQL SELECT 语句。尽管indexedDB规范中没有提到此类查询,但您可以通过使用由属性名称数组组成的键路径创建索引来近似复合查询的行为。
这与创建索引时使用多条目标志完全无关。多条目标志调整 indexedDB 如何在单个数组属性上创建索引。我们正在索引对象属性的数组,而不是对象的单个数组属性的值。
创建索引
在此示例中,“姓名”、“性别”和“年龄”对应于存储在学生对象存储中的学生对象的属性名称。
// An example student object in the students store
var foo = {
'name': 'bar',
'age': 15,
'gender': 'M'
};
function myOnUpgradeNeeded(event) {
var db = event.target.result;
var students = db.createObjectStore('students');
var name = 'males25';
var keyPath = ['name', 'gender', 'age'];
students.createIndex(name, keyPath);
}
在索引上打开光标
然后,您可以在索引上打开光标:
var students = transaction.objectStore('students');
var index = students.index('males25');
var lowerBound = ['AAAAA','male',26];
var upperBound = ['ZZZZZ','male',200];
var range = IDBKeyRange.bound(lowerBound, upperBound);
var request = index.openCursor(range);
但是,由于我将要解释的原因,这并不总是有效。
另外:使用范围参数来 openCursor 或 get 是可选的。如果您不指定范围,则IDBKeyRange.only
隐含地为您使用。换句话说,您只需要使用IDBKeyRange
有界游标。
基本指数概念
索引类似于对象存储,但不是直接可变的。相反,您在引用的对象存储上使用 CRUD(创建读取更新删除)操作,然后 indexedDB 自动将更新级联到索引。
理解排序是理解索引的基础。索引基本上只是一个经过特殊排序的对象集合。从技术上讲,它也被过滤了,但我稍后会谈到这一点。通常,当您在索引上打开游标时,您正在根据索引的顺序进行迭代。此顺序可能并且很可能与引用对象存储中的对象顺序不同。顺序很重要,因为这允许迭代更有效,并允许自定义下限和上限,这仅在特定于索引的顺序的上下文中才有意义。
索引中的对象在存储发生更改时进行排序。当您将对象添加到存储中时,它会添加到索引中的正确位置。排序归结为一个比较函数,类似于 Array.prototype.sort,它比较两个项目并返回一个对象是否小于另一个对象、大于另一个对象或相等。因此,我们可以通过深入了解比较函数的更多细节来更好地理解排序行为。
字符串按字典顺序进行比较
例如,这意味着“Z”小于“a”并且字符串“10”大于字符串“020”。
使用规范定义的顺序比较不同类型的值
例如,规范指定字符串类型值如何位于日期类型值之前或之后。值包含什么并不重要,只关心类型。
IndexedDB 不会为您强制类型。你可以在这里开枪打自己的脚。您通常永远不想比较不同的类型。
具有未定义属性的对象不会出现在其键路径由这些属性中的一个或多个组成的索引中
正如我所提到的,索引可能并不总是包含引用对象存储中的所有对象。当您将对象放入对象存储时,如果该对象缺少索引所基于的属性的值,则该对象将不会出现在索引中。例如,如果我们有一个不知道年龄的学生,并且我们将其插入到学生存储中,那么特定的学生将不会出现在 males25 索引中。
当您想知道为什么在索引上迭代游标时对象没有出现时,请记住这一点。
还要注意 null 和空字符串之间的细微差别。空字符串不是缺失值。具有空字符串的对象仍然可以出现在基于该属性的索引中,但如果该属性存在但未定义或不存在,则不会出现在索引中。如果它不在索引中,则在索引上迭代光标时将看不到它。
创建 IDBKeyRange 时必须指定数组键路径的每个属性
您必须为数组 keypath 中的每个属性指定一个有效值,当创建一个范围内的下限或上限以在该范围上打开游标时使用。否则,您将收到某种类型的 Javascript 错误(因浏览器而异)。例如,您不能创建一个范围,例如IDBKeyRange.only([undefined, 'male', 25])
因为 name 属性未定义。
令人困惑的是,如果您指定了错误的值类型,例如IDBKeyRange.only(['male', 25])
, 其中 name 未定义,您不会得到上述意义上的错误,但会得到无意义的结果。
这个一般规则有一个例外:您可以比较不同长度的数组。因此,从技术上讲,您可以从范围中省略属性,前提是您从数组的末尾开始这样做,并且适当地截断数组。例如,您可以使用IDBKeyRange.only(['josh','male'])
.
短路数组排序
indexedDB规范提供了一种对数组进行排序的显式方法:
Array 类型的值与 Array 类型的其他值进行比较,如下所示:
- 令 A 为第一个 Array 值,B 为第二个 Array 值。
- 设长度为 A 的长度和 B 的长度中的较小者。
- 让我为0。
- 如果 A 的第 i 个值小于 B 的第 i 个值,则 A 小于 B。跳过其余步骤。
- 如果 A 的第 i 个值大于 B 的第 i 个值,则 A 大于 B。跳过其余步骤。
- 将 i 增加 1。
- 如果 i 不等于长度,则返回步骤 4。否则继续下一步。
- 如果 A 的长度小于 B 的长度,则 A 小于 B。如果 A 的长度大于 B 的长度,则 A 大于 B。否则 A 和 B 相等。
关键在于第 4 步和第 5 步:跳过其余步骤。这基本上意味着如果我们比较两个数组的顺序,例如 [1,'Z'] 和 [0,'A'],该方法只考虑第一个元素,因为此时 1 > 0。它由于短路评估(规范中的步骤 4 和 5),永远不会检查 Z 与 A。
所以,前面的例子是行不通的。它实际上更像下面的工作:
WHERE (students.name >= 'AAAAA' && students.name <= 'ZZZZZ') ||
(students.name >= 'AAAAA' && students.name <= 'ZZZZZ' &&
students.gender >= 'male' && students.gender <= 'male') ||
(students.name >= 'AAAAA' && students.name <= 'ZZZZZ' &&
students.gender >= 'male' && students.gender <= 'male' &&
students.age >= 26 && students.age <= 200)
如果您对 SQL 或一般编程中的此类布尔子句有任何经验,那么您应该已经认识到不一定涉及全套条件。这意味着您将无法获得所需的对象列表,这就是您无法真正获得与 SQL 复合查询相同的行为的原因。
处理短路
在当前实现中,您无法轻易避免这种短路行为。在最坏的情况下,您必须将存储/索引中的所有对象加载到内存中,然后使用您自己的自定义排序功能对集合进行排序。
有一些方法可以最大限度地减少或避免一些短路问题:
例如,如果您使用 index.get(array) 或 index.openCursor(array),那么就没有短路问题。要么有完整的匹配,要么没有完整的匹配。在这种情况下,比较函数只评估两个值是否相同,而不是一个大于或小于另一个。
其他需要考虑的技术:
- 从最窄到最宽重新排列 keypath 的元素。基本上在范围上提供早期钳位,以切断一些不需要的短路结果。
- 将包装的对象存储在使用特殊自定义属性的存储中,以便可以使用非数组键路径(非复合索引)对其进行排序,或者可以使用不受短路影响的复合索引行为。
- 使用多个索引。这导致了爆炸指数问题。请注意,此链接是关于另一个 no-sql 数据库的,但相同的概念和解释适用于 indexedDB,并且该链接是一个合理(且冗长且复杂)的解释,因此我在此不再赘述。
- indexedDB(规范和 Chrome 实现)的创建者之一最近建议使用 cursor.continue:https ://gist.github.com/inexorabletash/704e9688f99ac12dd336
使用 indexedDB.cmp 进行测试
cmp 函数提供了一种快速简单的方法来检查排序的工作原理。例如:
var a = ['Hello',1];
var b = ['World',2];
alert(indexedDB.cmp(a,b));
indexedDB.cmp 函数的一个很好的特性是它的签名与Array.prototype.sort的函数参数相同。您可以轻松地从控制台测试值,而无需处理连接/模式/索引等。此外,indexedDB.cmp 是同步的,因此您的测试代码不需要涉及异步回调/承诺。