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

In IndexedDB, is there a way to make a sorted compound query?

提问人:jason 提问时间:8/23/2012 更新时间:7/9/2020 访问量:20192

问:

假设一张桌子有姓名、ID、年龄、性别、教育程度等,ID 是关键,并且该表格还为姓名、年龄和性别编制索引。我需要所有 25 岁以上的男学生按他们的名字排序。

这在mySQL中很容易:

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

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

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

html sqlite web-applications 客户端 indexeddb

评论


答:

1赞 Kristof Degrave 8/23/2012 #1

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

评论

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

在 indexedDB 中只能打开一个键范围查询。因此,请使用最有效的索引,在本例中为“age”。只需在光标迭代时过滤掉性别即可。您可以稍后使用 Array 迭代方法进行排序。IndexedDB API 除了预先排列索引条目外,对排序不感兴趣。

106赞 Josh 3/26/2013 #3

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

这与创建索引时使用多条目标志完全无关。multi-entry 标志调整 indexedDB 在单个数组属性上创建索引的方式。我们正在索引对象属性的数组,而不是对象的单个数组属性的值。

创建索引

在此示例中,“name”、“gender”和“age”对应于存储在 students 对象存储中的学生对象的属性名称。

// 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);

但是,由于我将要解释的原因,这并不总是有效。

旁白:使用 range 参数来 openCursor 或 get 是可选的。如果未指定范围,则隐式用于您。换句话说,您只需要用于有界游标。IDBKeyRange.onlyIDBKeyRange

基本指数概念

索引类似于对象存储,但不能直接变。相反,您可以对引用的对象存储使用 CRUD(创建、读取、更新、删除)操作,然后 indexedDB 会自动将更新级联到索引。

了解排序是理解索引的基础。索引基本上只是一个经过特殊排序的对象集合。从技术上讲,它也是经过过滤的,但我稍后会谈到这一点。通常,当您在索引上打开游标时,您正在根据索引的顺序进行迭代。此顺序可能(也可能)与引用对象存储中对象的顺序不同。顺序很重要,因为这样可以提高迭代效率,并允许自定义下限和上限,该下限和上限仅在特定于索引的顺序的上下文中才有意义。

索引中的对象在存储发生更改时进行排序。将对象添加到存储区时,该对象将添加到索引中的适当位置。排序归结为一个比较函数,类似于 Array.prototype.sort,该函数比较两个项目并返回一个对象是否小于另一个对象、大于另一个对象或等于另一个对象。因此,我们可以通过深入研究比较函数的更多细节来更好地理解排序行为。

按字典比较字符串

例如,这意味着“Z”小于“a”,字符串“10”大于字符串“020”。

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

例如,规范指定字符串类型值如何出现在日期类型值之前或之后。值包含什么并不重要,重要的是类型。

IndexedDB 不会强制类型。你可以在这里搬起石头砸自己的脚。您通常永远不想比较不同的类型。

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

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

当您想知道为什么在索引上迭代游标时不显示对象时,请记住这一点。

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

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

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

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

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

短路阵列分选

indexedDB 规范提供了对数组进行排序的显式方法:

将 Array 类型的值与 Array 类型的其他值进行比较,如下所示:

  1. 设 A 是第一个 Array 值,B 是第二个 Array 值。
  2. 设长度是 A 的长度和 B 的长度中的较小者。
  3. 设 i 为 0。
  4. 如果 A 的第 i 个值小于 B 的第 i 个值,则 A 小于 比 B. 跳过其余步骤。
  5. 如果 A 的第 i 个值大于 B 的第 i 个值,则 A 大于 B。
  6. 将 i 增加 1。
  7. 如果 i 不等于长度,请返回步骤 4。否则,请继续执行下一步。
  8. 如果 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),则不存在短路问题。要么有整场比赛,要么没有整场比赛。在这种情况下,比较函数仅评估两个值是否相同,而不是一个值是否大于或小于另一个值。

其他需要考虑的技术:

  • 将键路径的元素从最窄到最宽重新排列。基本上,在范围上提供早期钳位,以切断一些不需要的短路结果。
  • 将包装对象存储在使用特殊自定义属性的存储中,以便可以使用非数组键路径(非复合索引)对其进行排序,或者可以使用不受短路行为影响的复合索引。
  • 使用多个索引。这导致了索引爆炸问题。请注意,此链接是关于另一个 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 是同步的,因此您的测试代码不需要涉及异步回调/承诺。

评论

1赞 rgardler 4/12/2013
请注意,IE10 和 Windows 8 目前不支持此功能。
0赞 buley 3/11/2014
这是一个非常聪明的解决方案,尽管我还不相信这是正确的行为。无论如何,在这里很有用。在创建索引时,它会表示是为数组中的每个项目添加单行还是多行。multiEntry
1赞 dumbmatter 10/5/2014
这是一个很棒的帖子。关于查询具有较短数组的复合索引的部分的后续问题:stackoverflow.com/questions/26203075/...
0赞 Brett Zamir 6/10/2017
(students.name >= 'AAAAA' && students.name <= 'ZZZZZ') ||将导致所有学生被匹配,无论接下来是什么......我想你想要一个最后的短路,不是吗?&&
0赞 Josh 6/10/2017
@BrettZamir不完全是,无论接下来是什么,所有学生都匹配是问题所在。
2赞 Kevin 7/10/2016 #4

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

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

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

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

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

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

2赞 Uday 9/5/2017 #5

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

这是使用 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 编写即可。希望这有帮助!

评论

0赞 kamalav 2/8/2018
嗨,我怎样才能选择特定的列,而不是使用它选择所有古拉姆。
0赞 Uday 2/9/2018
查询结果包含所有列,您必须根据 OnSuccess 方法中的要求操作结果数组。
0赞 aggregate1166877 6/19/2023
您发布的教程链接已失效。不幸的是,官方文档是一堆完全无法阅读的脑洞大开的垃圾。