Статьи

Поиск элементов массива в IndexedDB

Продолжая исследование IndexedDB , сегодня я решил посмотреть, как можно выполнить поиск данных на основе свойства массива. Поиск — для меня — самая критическая слабость IndexedDB сейчас. Но «слабый» не означает, что он бессилен, и, может быть, я еще не в полной мере оцениваю мощь IndexedDB. Сегодня мне было особенно интересно узнать о следующем сценарии использования:

Представьте себе объект с именем Person. Этот объект содержит ряд свойств, но пока представьте, что он содержит только поле с именем name и поле с именем tags. Имя — очевидно — имя. Теги — это массив строк. Вот несколько простых примеров:

{
  name:"Ray",
  tags:["apple","banana","beer"]
}

{
  name:"Scott",
  tags:["beer"]
}

{
  name:"Marc",
  tags:["mongo","jenkins"]
}

В данных выше мы определили трех человек. Обратите внимание на массив тегов, назначенных каждому человеку. Я хотел знать, как я буду искать конкретный тег. Например, как мне найти людей с тегом «пиво»?

Я начал свой тест с написания логики установки IndexedDB. (В качестве примечания: я не делал «хак», чтобы заставить этот код работать в Chrome. Хак не так уж плох. Я написал об этом здесь . Но так как я не хотел слишком сильно запутывать свой код Я сосредоточился на написании спецификации и протестировал ее в Firefox.) Обратите внимание, что у моего хранилища объектов есть два индекса. Один для имени и один для тегов.

var openRequest = indexedDB.open("testcomplex3",1);

openRequest.onupgradeneeded = function(e) {

var thisDb = e.target.result;

//Create objectStore
if(!thisDb.objectStoreNames.contains("people")) {
var objectStore = thisDb.createObjectStore("people", { keyPath: "id", autoIncrement:true });
objectStore.createIndex("name","name", {unique:false});	
objectStore.createIndex("tags","tags", {unique:false});	
}

}

Затем я набрал простую форму, которая позволила бы мне вводить данные, нажимать кнопку и добавлять их в мое хранилище объектов. Форма достаточно проста, чтобы я мог пропустить показ HTML, но вот как я справляюсь с действием в JavaScript.

function addData(e) {
e.preventDefault();

var transaction = db.transaction(["people"], "readwrite");
var objectStore = transaction.objectStore("people");
var name = $("#name").val();
var tags = $("#tags").val().split(",");
console.log("adding "+name+" with tags "+tags);
var req = objectStore.add({name:name, tags:tags});
req.onsuccess = function() {
console.log("Data added");
};
}

Единственное, что я буду вызывать, это разделенный вызов по значению тегов. Это позволяет мне вводить списки в поле формы («a, b») и преобразовывать их в массив для хранения. Для моего тестирования я сделал 3 человека. Один с тегом A, один с тегами A и B и один с тегом C.

Наконец — я написал супер быструю утилиту ‘dump’, которая записывала данные в div. Это было сделано, чтобы написать все и дать мне что-то для сравнения, когда я делал свои поиски.

function showAll(e) {
e.preventDefault();
var transaction = db.transaction(["people"], "readonly");
var objectStore = transaction.objectStore("people");
var request = objectStore.openCursor();
var s = "";

request.onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
s += "<h2>Key "+cursor.key+"</h2><p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
$("#results").html(s);
}	
}

Итак, в этот момент у меня была форма, позволяющая мне вводить данные, и кнопка, которую я мог нажать, чтобы быстро перечислить, какие данные существовали в хранилище объектов. Теперь мне нужно было искать.

Я добавил на свою страницу простое текстовое поле и кнопку, которые позволили бы мне ввести тег и запустить функцию. Получение данных из хранилища объектов IndexedDB может быть выполнено двумя основными способами. Вы либо запрашиваете данные, основанные на определенном значении (люди, где имя = луч), либо запрашиваете диапазон данных (люди, у которых имя начинается с буквы А и выше). Оба могут вернуть один или несколько результатов. Я начал с простой операции типа get:

function getByTag(e) {
e.preventDefault();
var tag = $("#tag").val();

var transaction = db.transaction(["people"], "readonly");
var objectStore = transaction.objectStore("people");
var index = objectStore.index("tags");

var test = index.get(tag);

test.onsuccess = function(e) {
console.log('match call');
var match = e.target.result;
if(match) {
console.log("Match");
console.dir(match);
}
}
}

По большей части это должно иметь смысл (если вы знаете что-нибудь о IndexedDB или читали мои предыдущие записи). Основным камнем преткновения является строка 9. Вы можете видеть, как я передаю строку в вызов get. Это «чувствуется» неправильно для меня, так как значение тега является массивом. Я все равно попробовал. Как я и ожидал, это не удалось.

Затем я изменил код для передачи массива в API get:

function getByTag(e) {
e.preventDefault();
var tag = $("#tag").val();
var tagArray = tag.split(",");

var transaction = db.transaction(["people"], "readonly");
var objectStore = transaction.objectStore("people");
var index = objectStore.index("tags");

var test = index.get(tagArray);

test.onsuccess = function(e) {
console.log('match call');
var match = e.target.result;
if(match) {
console.log("Match");
console.dir(match);
}
}

}

И вуаля — это сработало … вроде . Если бы я искал А, это сработало бы для человека, у которого только А был тегом. Это не будет соответствовать человеку с A и B в качестве тегов. На самом деле, я даже не мог найти B. Но C сделал работу. Так что мне казалось, что я не могу найти совпадение во втором элементе или совпадение с человеком, у которого было больше одного элемента. Если вы хотите убедиться в этом сами, укажите свой браузер Firefox на http://www.raymondcamden.com/demos/2012/aug/10/test4.html .

Поэтому в этот момент я сделал паузу и сделал что-то сумасшедшее … перечитал спецификацию немного. В частности, часть, относящаяся к созданию индексов. Оказывается, есть опция, специально предназначенная для массивов. Именно этот пункт помог мне понять кое-что:


Индекс также содержит флаг multiEntry.
Этот флаг влияет на поведение индекса, когда в результате оценки пути ключа индекса получается массив. Если флаг multiEntry имеет значение false, то в индекс добавляется одна запись, ключом которой является массив. Если флаг multiEntry имеет значение true, то одна запись добавляется в индекс для каждого элемента в массиве. Ключом для каждой записи является значение соответствующего элемента в массиве.

Ах ах! Я обновил свой код создания индекса, чтобы включить эту опцию:

objectStore.createIndex («теги», «теги», {unique: false, multiEntry: true});

Я тогда сделал больше тестирования. Вышеуказанное изменение само по себе было недостаточно. Теперь, когда я знал, что индекс хранится по элементам, я удалил код, который разделил мой ввод в массив. В основном я вернулся к этому:

var tag = $("#tag").val();

var transaction = db.transaction(["people"], "readonly");
var objectStore = transaction.objectStore("people");
var index = objectStore.index("tags");

var test = index.get(tag);

И это вернуло меня туда, где я был раньше. Совпадающий человек с тегом A, но не тот, у кого A является одним из многих тегов. Я чувствовал, что я был на правильном пути, хотя, поэтому я взял другой курс. Что делать, если я сделал поиск диапазона? Один из вариантов поиска по диапазону — сопоставить только одну вещь — что звучит немного запутанно. Но я понял, какого черта. В приведенном ниже коде я переключился на диапазон и использовал единственный оператор:

function getByTag(e) {
e.preventDefault();
var tag = $("#tag").val();

var transaction = db.transaction(["people"], "readonly");
var objectStore = transaction.objectStore("people");
var index = objectStore.index("tags");

var s = "";

var rangeTest = IDBKeyRange.only(tag);
index.openCursor(rangeTest).onsuccess = function(e) {
var cursor = e.target.result;
if (cursor) {
s += "<h2>Key "+cursor.key+"</h2><p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
$("#searchresults").html(s);
}

}

И это сработало. Я имею в виду, конечно, это сработало. Это позволило мне найти А и найти оба результата. Это также позволило мне найти B и найти человека, у которого он был вторым тегом.

Я не уверен на 100%, почему я понял, почему был необходим поиск по диапазону, но он не кажется мне слишком вялым. Для тех из вас, у кого нет современного Firefox, вот быстрый снимок экрана дисплея:

А для тех, кто хочет протестировать, нажмите кнопку демо ниже и наслаждайтесь. Я извиняюсь, если этот пост немного запутан. Пожалуйста, не стесняйтесь задавать любые вопросы или исправлять ошибки в комментариях!