Продолжая исследование 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, вот быстрый снимок экрана дисплея:
А для тех, кто хочет протестировать, нажмите кнопку демо ниже и наслаждайтесь. Я извиняюсь, если этот пост немного запутан. Пожалуйста, не стесняйтесь задавать любые вопросы или исправлять ошибки в комментариях!