在 IndexedDB 中,有没有办法进行排序复合查询?

Posted

技术标签:

【中文标题】在 IndexedDB 中,有没有办法进行排序复合查询?【英文标题】:In IndexedDB, is there a way to make a sorted compound query? 【发布时间】:2012-08-18 12:41:51 【问题描述】:

假设一个表有名称、ID、年龄、性别、教育等。ID 是键,表还索引名称、年龄和性别。我需要所有 25 岁以上的男学生,按姓名排序。

这在 mysql 中很容易:

    SELECT * FROM table WHERE age > 25 AND sex = "M" ORDER BY name

IndexDB 允许创建索引并根据该索引对查询进行排序。但它不允许像年龄和性别这样的多个查询。我找到了一个名为 queryIndexedDB (https://github.com/philikon/queryIndexedDB) 的小型库,它允许复合查询,但不提供排序结果。

那么有没有办法在使用 IndexedDB 的同时进行排序复合查询?

【问题讨论】:

【参考方案1】:

尝试使用Linq2indexedDB 这个库允许您使用多个过滤器、多个排序,甚至从对象中选择数据。它也适用于跨浏览器(IE10、Firefox 和 Chrome)

【讨论】:

我在使用 Linq2indexedDB 时出现了巨大的内存泄漏。我看到这里记录了这个问题:linq2indexeddb.codeplex.com/workitem/23451,但从未解决。所以我不得不重写我的应用程序而不使用这个框架。 无法解决它,因为我无法重现该问题。如果你有一些额外的信息,我可以再看一遍。为了避免泄漏,您需要做的一件事是关闭日志记录。这是因为日志记录了您正在使用的所有对象(更易于调试),但缺点是它正在泄漏内存。【参考方案2】:

您只能在 indexedDB 中打开 open one key range query。所以使用最有效的索引,在这种情况下,“年龄”。只需在光标迭代时过滤掉性别。您可以稍后使用数组迭代方法进行排序。 IndexedDB API 除了预先安排索引条目外,对排序没有兴趣。

【讨论】:

【参考方案3】:

此答案中使用的术语复合查询是指在其 WHERE 子句中涉及多个条件的 SQL SELECT 语句。尽管 indexedDB 规范中未提及此类查询,但您可以通过使用由属性名称数组组成的 keypath 创建索引来近似复合查询的行为。

这与创建索引时使用多条目标志完全无关。多条目标志调整 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' 并且 string '10' 大于 string '020'。

使用规范定义的顺序比较不同类型的值

例如,规范指定字符串类型值如何位于日期类型值之前或之后。值包含什么无关紧要,类型无关紧要。

IndexedDB 不会为您强制类型。你可以在这里开枪打自己的脚。您通常永远不想比较不同的类型。

具有未定义属性的对象不会出现在其键路径由这些属性中的一个或多个组成的索引中

正如我所提到的,索引可能并不总是包含引用对象存储中的所有对象。当您将对象放入对象存储时,如果该对象缺少索引所基于的属性的值,则该对象将不会出现在索引中。例如,如果我们有一个不知道年龄的学生,并且我们将其插入到学生存储中,那么特定的学生将不会出现在 males25 索引中。

当您想知道为什么在索引上迭代光标时对象没有出现时,请记住这一点。

还要注意 null 和空字符串之间的细微差别。空字符串不是缺失值。具有空字符串属性的对象仍可能出现在基于该属性的索引中,但如果该属性存在但未定义或不存在,则不会出现在索引中。如果它不在索引中,则在索引上迭代光标时将看不到它。

您必须在创建 IDBKeyRange 时指定数组键路径的每个属性

您必须为数组键路径中的每个属性指定一个有效值,当创建一个下限或上限以在某个范围内打开游标时使用该范围。否则,您将收到某种类型的 javascript 错误(因浏览器而异)。例如,您不能创建诸如 IDBKeyRange.only([undefined, 'male', 25]) 之类的范围,因为 name 属性未定义。

令人困惑的是,如果你指定了错误的type值,例如IDBKeyRange.only(['male', 25]),其中name是未定义的,你不会得到上述意义上的错误,但你会得到无意义的结果.

这个一般规则有一个例外:您可以比较不同长度的数组。因此,从技术上讲,您可以从范围中省略属性,前提是您从数组的 end 开始这样做,并且适当地截断数组。例如,您可以使用IDBKeyRange.only(['josh','male'])

短路数组排序

indexedDB specification 提供了一种对数组进行排序的显式方法:

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),则没有短路问题。要么有完整的匹配,要么没有完整的匹配。在这种情况下,比较函数只评估两个值是否相同,而不是一个大于还是小于另一个。

其他需要考虑的技术:

从最窄到最宽重新排列键路径的元素。基本上在范围上提供早期钳位,以切断一些不需要的短路结果。 将包装的对象存储在使用特殊自定义属性的存储中,以便可以使用非数组键路径(非复合索引)对其进行排序,或者可以使用不受短路行为。 使用多个索引。这导致exploding index problem。请注意,此链接是关于另一个 no-sql 数据库的,但相同的概念和解释适用于 indexedDB,并且该链接是一个合理(且冗长且复杂)的解释,因此我不再在这里重复。 indexedDB(规范和 Chrome 实现)的创建者之一最近建议使用 cursor.continue:https://gist.github.com/inexorabletash/704e9688f99ac12dd336

使用 indexedDB.cmp 进行测试

cmp function 提供了一种快速简单的方法来检查排序的工作原理。例如:

var a = ['Hello',1];
var b = ['World',2];
alert(indexedDB.cmp(a,b));

indexedDB.cmp 函数的一个很好的特性是它的签名与Array.prototype.sort 的函数参数相同。您可以轻松地从控制台测试值,而无需处理连接/模式/索引等。此外,indexedDB.cmp 是同步的,因此您的测试代码不需要涉及异步回调/承诺。

【讨论】:

请注意,IE10 和 Windows 8 目前不支持此功能。 这是一个非常聪明的解决方案,虽然我还不相信这是正确的行为。无论如何,multiEntry 在这里很有用。用于创建索引时,表示为数组中的每个项目添加单行还是多行。 这篇文章很棒。关于使用较短数组查询复合索引的部分的后续问题:***.com/questions/26203075/… (students.name &gt;= 'AAAAA' &amp;&amp; students.name &lt;= 'ZZZZZ') || 会导致所有学生都被匹配,无论接下来发生什么......我想你想要一个 &amp;&amp; 在最后短路,不是吗? @BrettZamir 不完全是,所有匹配的学生,无论接下来发生什么都是问题。【参考方案4】:

我晚了几年,但我只想指出,乔希的回答只考虑查询中的“列”是索引keyPath 的一部分的情况。

如果任何上述“列”存在于索引的keyPath 之外,则必须在示例中创建的游标迭代的每个条目上测试涉及它们的条件。因此,如果您正在处理此类查询,或者您的索引不是unique,请准备好编写一些迭代代码!

无论如何,如果您可以将查询表示为布尔表达式,我建议您查看BakedGoods。

对于这些类型的操作,它将始终在焦点 objectStore 上打开一个游标,除非您执行严格的相等查询(x ===? y,假设 x 是 objectStore 或索引键),但它会为您省去麻烦编写自己的光标迭代代码:

bakedGoods.getAll(
    filter: "keyObj > 5 && valueObj.someProperty !== 'someValue'",
    storageTypes: ["indexedDB"],
    complete: function(byStorageTypeResultDataObj, byStorageTypeErrorObj)
);

为了完全透明,BakedGoods 由 moi 维护。

【讨论】:

【参考方案5】:

有一个库 JsStore 可用于从 IndexedDB 查询数据,它非常易于使用并且节省了大量代码和时间。 您可以通过here了解更多信息

这是使用 JsStore 的等效 sql 查询。

var connection = new JsStore.Instance("DbName");

connection.select(
    From: "TableName",
    Where: 
        age :  '>':'25',
        sex : 'M'
    ,
    Order: 
        By: 'Name'
    ,
    OnSuccess:function (results)
        console.log(results);
    ,
    OnError:function (error) 
        console.log(error);
    
);

用 Sql 思考,用 JS 编写。希望这会有所帮助!

【讨论】:

嗨,我如何选择特定列而不是使用此选择所有列。 查询结果包含所有列,您必须根据 OnSuccess 方法中的要求操作结果数组。

以上是关于在 IndexedDB 中,有没有办法进行排序复合查询?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Firefox 中查看 IndexedDB 内容

对 indexedDB 查询的结果进行排序

web网站使用indexedDB缓存大数量案例

如何创建 indexeddb 组合键

如何对复合对象中的记录进行排序?

对复合集合进行排序