PersistJS и TaffyDB. Как поселить почти настоящую базу данных в браузер. Часть 3

June 22, 2009

Эта статья завершит собой серию материалов, рассказывающих о том, как можно внутри обычного браузера “поселить” базу данных. “Браузерная” СУБД должна содержать две функции: сохранение данных и операции над ними (поиск, редактирование). В прошлых двух статьях я рассказывал о том, как библиотека persistjs позволяет организовать унифицированное хранение информации не зависимо от версии используемого браузера. Сегодня мы поговорим о том, что мы можем делать с сохраненной информацией.

Для того, чтобы функция хранения данных работала всегда и во всех браузерах нам пришлось отказаться от использования функциональности google gears или whatwg_db. И, следовательно, пришлось отказаться от представления информации в виде таблиц. Все что у нас есть это тройка функций: сохранить строку текста, прочитать ее из хранилища и удалить. В практике, хранение “обычной” строки текста малополезно: обычно данные представляют более сложные структуры: массивы или объекты, а еще чаще их комбинацию. А значит, нам нужен механизм “сохранения” произвольного объекта в строку, чтобы затем эту строку подать на вход persistjs или отправить запрос сохранения на сервер. Также нужен и обратный механизм восстановления из строкового представления оригинального javascript-объекта. На роль подобного средства идеальным образом подходит JSON. Вторым этапом, после того как мы получили на руки массив объектов (записей), нам потребуются средства выполнять над этими данными операции поиска и изменения данных. Например, обновление данных по критериям, удаление данных или добавление в “в как бы таблицу” новых записей. Т.е. то, что называется термином CRUD – Create Read Update Delete. Сегодня я расскажу об двух известных javascript-библиотеках, решающих все перечисленные выше задачи: taffydb и jlinq. И начнем мы с более простой – taffydb.

Домашний сайт библиотеки taffy - это http://taffydb.com/. Там вы можете скачать архив с самой библиотекой (размер всего 10 килобайт), а на странице http://taffydb.com/index.cfm?oa=gettingstarted увидеть краткую справку с перечислением основных возможностей taffydb. Taffy – молодой проект: первая версия (1.0) появилась еще весной 2008 г, а за прошедшее время счетчик версий успел дойти до цифры 1.7. Хотя, последние версии носят в основном характер “исправления ошибок” и новая функциональность не добавляется. Итак, после того как вы скачали библиотеку, разархивировали и подключили ее к вашему html-файлу, самое время чтобы создать “Коллекцию объектов”. Taffy, по своей сути, всего лишь набор функций, которые выполняют работу над набором объектов-записей. Каждая запись представляет собой набор пар: название характеристики и ее значение. Вот пример массива записей с информацией о людях:
  1. var users = [
  2.   {fio: 'Vasya', weight: 80},
  3.   {fio: 'Petya', weight: 60}
  4. ];
Такой массив данных вряд ли будет присутствовать на html-странице – более вероятен сценарий, когда с сервера будет загружена текстовая строка с JSON представлением массива. А значит, нам нужен способ выполнить преобразование массива в строку JSON и обратно. Почти все javascript библиотеки имеют подобную функциональность, например, библиотека Yahoo UI, о которой я недавно закончил серию статей, содержит функции parse и stringify:
  1. // преобразуем строку в JSON-объект
  2. var json = YAHOO.lang.JSON.parse (oResponse.responseText);
  3. // и обратное преобразование объекта в строку
  4. alert (YAHOO.lang.JSON.stringify(json));
К счастью, нет никакой необходимости “тащить” в проект еще какую-нибудь javascript-библиотеку ради пары функций: т.к. в состав taffy входит функции работы с JSON:
  1. // так я преобразую массив объектов в строку
  2. var s = TAFFY.JSON.stringify(users);
  3. // а теперь преобразуем строку в набор объектов
  4. alert ( TAFFY.JSON.parse(s) );
Теперь вернемся к задаче вынесенной в заголовок статьи – поиску, фильтрации, сортировке данных. Для этого я создаю объект taffy, передав на вход конструктору либо JSON массив объектов, либо строку:
  1. var t = new TAFFY(users);
Теперь от имени переменной “t” я могу вызывать функцию find, передав ей в качестве параметра специальный объект – шаблон поиска. Так в следующем примере я найду всех людей, вес которых равен 50, 60 и 70 килограмм:
  1. t.find({weight: [50, 60, 70]});
Здесь и далее, когда мы записываем условие, то оно помещается внутрь фигурных скобок, т.е. условие – это JSON-объект. Полями являются названия полей записей в массиве (“fio” или “weight”), по которым идет поиск. А значение поля кодирует накладываемое на поле условие сравнения. Результатом вызова метода find будет массив с порядковыми номерами записей, которые подошли под условие, т.е. номера 1 и 2. Имя в своем распоряжении порядковые номера записей, добраться до их содержимого проще простого, однако если вы хотите сразу получить не номера объектов, а их значения, то используйте вызов метода get:
  1. t.get({weight: [50, 60, 70]});
В случае если вас интересует не весь список, а только первый или последний из найденных элементов, то можно сделать так:
  1. t.first({weight: [50, 60, 70]});
  2.  
  3. //или если нужен последний элемент:
  4. t.last({weight: [50, 60, 70]});
В том случае, если не было найдено ни одного элемента подходящего условию, то функции first и last вернут false. Если мы не передадим никакого условия-фильтра при вызове методов find или get, то taffy вернет массив со всеми индексами элементов или массив всех объектов коллекции. Для того, кто знаком с тем, как записываются условия поиска в “настоящих базах данных” и ожидавшим увидеть ключевые слова “WHERE, ORDER BY”, стиль поиска “по примеру” может показаться непривычным. В действительности, т.к. у нас не реляционная СУБД, т.е. нет набора взаимосвязанных таблиц с данными – а только одна таблица-массив. То большинство запросов сведутся к условиям-фильтрам, накладываемым на значения полей. А значит, нет необходимости внедрять в браузер какое-то подобие языка SQL (к слову сказать, вторая библиотека jlinq как раз содержит средства выполнять запросы с участием двух объединяемых таблиц). К тому же последние тенденции в сфере программирования как раз и сводятся к тому, чтобы дать возможность писать тексты запросов непосредственно внутри основного языка программирования, а не формировать строку запроса SQL. В качестве примеров можно привести ruby/rails/active record, затем groovy/grails/gorm или linq из microsoft .net. В любом случае, taffy позволяет записывать фильтры, в которых есть не только простейшее сравнение на равенство или не равенство, но и операции сравнения. Так в следующих примерах показываются все простые операторы.
  1. t.find({weight: {equal : 50} })
  2. t.find({weight: {is: 50} })
Equal и is являются синонимами и служат для того, чтобы задать точное условие на равенство. Для отношений больше-меньше мы используем операторы: “greaterthan” и или сокращение “gt” (“больше чем”):
  1. t.find({weight: {greaterthan: 50} })
По аналогии используется оператор сравнения “меньше чем” (lessthan или lt):
  1. t.find({weight: {lt: 50} })
В том случае, если вы хотите записать обратное условие, например, найти всех людей, у которых вес не равен 50 кг, то нужно записать перед оператором сравнения символ знака восклицания, т.е. “!is” означает “не равно” (также знак “!” можно комбинировать и с операторами “lt” и “gt”):
  1. t.find({weight: {"!is": 50} })
Для работы со строками, оператора сравнения “is” мало – нужны средства сравнения строк по шаблону или по регулярному выражению. И в taffy данная функциональность есть. Оператор “starts” или “startswith” помогут найти всех людей, имена которых начинаются с букв “Pet”:
  1. t.find({fio: {"startswith": "Pet" } })
Если же мы хотим найти тех, чья фамилия заканчивается на буквы “rov”, то используем оператор “endswith” или “ends”. Если стоит задача поиска в ФИО определенного буквосочетания, но при этом нет разницы будет ли оно находится в начале, в середине или конце строки, то используем оператор “like”. К сожалению, в отличие от оператора like из мира баз данных и SQL, мы не можем использовать специальные символы подстановки “?”, “*” (“_” и “%”):
  1. t.find({fio: {like: "Pet" } })
Раз функция like не поможет нам в записи сложных шаблонов соответствия строк, то остается только воспользоваться регулярными выражениями:
  1. t.find ({fio: {regex: /vasya/i} })
Так в показанном примере я нашел всех сотрудников, в ФИО которых присутствовало слово “vasya” без учета регистра. Запись /vasya/i – является стандартным для javascript способом создания Regexp (и taffy не привносит здесь ничего от себя).

Т.к. taffy предоставляет функции поиска только над одним единственным массивом записей; и мы не можем хранить данные в нескольких связанных между собой таблицах. То приходится создавать такие объекты-записи, что некоторые из их полей не строки, числа или даты, а снова массивы или объекты. Например, для каждого из сотрудников из предыдущего примера я хочу добавить сведения о том, какие фрукты он любит:
  1. var users = [
  2.   {fio: 'Vasya', weight: 80, likes: ["Apple", "Orange", "Grapes"] } 
  3. ];
Теперь если я хочу найти тех сотрудников, которые предпочитаю яблоки, то нужно записать запрос поиска так:
  1. t.find ({likes: {has: "Apple"} })
Если же мне нужно узнать, любит ли сотрудник не только яблоки, но еще апельсины и виноград, то я использую оператор сравнения hasAll:
  1. t.find ({likes: {hasAll: ["Apple", “Orange”, “Grapes”]} })
Данные можно не только отбирать на основании условий, но и сортировать. Для этого я использую функцию orderBy, передавая ей как параметр массив имен полей, по которым нужно выполнить сортировку. После того как коллекция taffy была отсортирована можно применять к ней условия поиска.
  1. t.orderBy ([“weight”, “fio”])
  2. // а так можно поменять направление сортировки на обратное
  3. t.orderBy ([weight: “desc”])
  4. // и выведем всю отсортированную коллекцию на экран
  5. alert ( t.get() );
В тех случаях, когда условие сортировки сложное и его нельзя записать просто как перечень имен полей, то можно как параметр вызова метода orderBy передать собственную функцию сравнения, например, так:
  1. function mySort (a, b){
  2.  if (a.fio < b.fio) return +1;
  3.  if (a.fio > b.fio) return -1;
  4.  return 0;
  5. }
  6.  
  7. t2.orderBy(mySort);
Теперь рассмотрим примеры того как можно изменять информацию внутри коллекции taffy. Так для удаления записей по критерию используется функция remove, ее параметры задаются по точно таким же правилам, как и условия поиска для функций get и find:
  1. t.remove ({weight: 50})
Для того, чтобы вставить новую запись мы используем функцию insert, в качестве параметра для которой передается либо отдельная запись или их массив. В любом случае новые записи будут добавлены в конец существующей коллекции:
  1. t.insert ({fio: 'Ronald', weight: 80})
Редактирование записей также построено на идее объектов-шаблонов, как для условия поиска, так и для правил редактирования. Так в следующем примере я установлю новое значение веса Ronald-а:
  1. t.update ({weight: 100}, {fio: “Ronald”})
Все описанные выше примеры были очень простые и удобно записывались с помощью taffy фильтров, но как только сложность поисковых запросов растет, картина резко ухудшается. Прежде всего, неудобно записывать условия поиска, в которых на одно поле накладывается несколько условий, например, мы хотим найти всех людей, у которых вес менее 100 кг, но не равен при этом 50 кг. Так первым шагом я нашел всех людей, вес которых менее 100 кг, а затем полученный список порядковых номеров был подан второй раз на вход функции find:
  1. var found1 = t.find ( {weight: {lt: 100}  } );
  2. // ограничиваем поиск теми кого мы нашли в прошлый раз
  3. var found2 = t.find ({ weight: {"!is": 50}}, found1)
  4. // и выводим найденные записи на экран
  5. alert (t.get(found2));
Если вам нужно записать несколько условий через оператор OR (или), то опять таки придется комбинировать несколько вызовов метода find, не забыв при этом избавиться от возможных дубликатов:
  1. var found1 = t.find ({ weight: {"lt": 50}})
  2. var found2 = t.find ({ fio: “Vasya”})
  3. alert (TAFFY.gatherUniques (found1.concat(found2)));
Что более неприятно, taffy не умеет корректно обрабатывать условия поиска по составным объектам. К примеру, у каждого из сотрудников ест адрес, состоящий из города и номера дома. Нам нужно найти всех людей живущих в г. Минске. К сожалению, нельзя записать что-то вроде:
  1. t.find ({“address.city: “minsk”})
Для подобных поисковых запросов, нужно использовать “последнее” средство – возможность передать внутрь метода find не объект шаблон поиска, а функцию сравнения.

Как вывод: taffy очень “легкая” и простая для освоения “обертка” над коллекцией элементов, позволяющая выполнять поиск данных по несложным критериям, а также изменять содержимое коллекции элементов. В случае, если вам нужны поисковые запросы, оперирующие сложными комбинациями условий AND или OR, а также выполняющие поиск по вложенным элементам. Например, у сотрудника есть адрес, состоящий из города и номера дома, и мы хотим задать поиск по названию улицы. То всех этих случаях вам больше подойдет другая библиотека – jlinq.

Домашний сайт проекта Jlinq - http://www.hugoware.net/. Размеры библиотек jlinq и taffy практически одинаковы: и там и там 10 кб в сжатом виде. Вот только возможностей у jlinq побольше за счет того, что она не содержит функций по конвертации данных в JSON формат. Итак, я предполагаю, что вы загрузили библиотеку и подключили ее к своему html-файлу. В качестве примера данных я воспользуюсь тем же массивом записей о сотрудниках, что и в примере с taffy. Предполагая, что в переменной users хранятся данные, я могу записать так:
  1. var results = jLinq.from(users) 
  2.   .startsWith("fio", "Vas") 
  3.   .or("Pet") 
  4.   .orderBy("weight") 
  5.   .select();
Этот пример кода демонстрирует общую методику работы с jlinq: все выполняемое выражение состоит из четырех последовательных частей. Во-первых, нужно указать на источник записей за это отвечает конструкция jLinq.from. Потом идет произвольное количество условий отбора (вторая часть запроса – это команды отбора) связанные между собой логическими операторами (третья часть это ключевые слова and, or, not). Четвертая часть выражения jlinq – служит для указания того, что нужно сделать. В примере слово select говорит, что нужно найти все записи соответствующие записанному условию отбора. Кроме select еще есть first и last (для получения первой и последней записи подошедшей по условию). Оператор orderBy позволяет записать условия сортировки. В случае, если мы хотим задать сортировку по какому-то из полей в обратном порядке то перед именем поля ставится знак “-”:
  1. .orderBy("-weight", "fio")
Еще пример часто используемого оператора – это count, он вернет количество найденных записей. Если мы хотим избавиться от дублирующихся записей, то поможет команда distinct. Так в следующем примере я получил массив с уникальными значениями веса людей.
  1. jLinq.from(users) 
  2.   .distinct("weight");
Каждое условие отбора данных записывается в виде функции или оператора сравнения, первым параметром для которого идет имя поля, а вторым - значение с которым нужно выполнить сравнение. Также как и для taffy, если мы записываем последовательно несколько операторов, например, startsWith, затем endsWith, то эти операторы связываются между собой оператором AND. Вызов функции and() может быть либо явным как в следующем примере либо предполагаться по-умолчанию:
  1. jLinq.from(users) 
  2.   .startsWith("fio", "Vas") 
  3.   .and() 
  4.   .greater("weight", 200) 
  5.   .orderBy("weight") 
  6.   .select();
Если же мы хотим перечислить условия через ИЛИ (OR), то можно либо записать условие так:
  1. jLinq.from(users)
  2.   .startsWith("fio","Vas")
  3.   .or()
  4.   .startsWith("fio","Pet")
  5.   .select();
Но, т.к. это выглядит несколько громоздко, то jlinq представляет функцию повторения выражения. Так, когда я записываю оператор startsWith, то он сохраняется вместе со своим первым аргументом – именем поля. И в дальнейшем я могу записать внутри оператора “or” всего одно значение, т.е. то с чем нужно выполнить сравнение.
  1. jLinq.from(users) 
  2.   .startsWith("fio","Vas")
  3.   .or("Pet")
  4.   .select();
К слову сказать, приятным отличием jlinq от taffy является гибкость многих стандартных операторов сравнения. Так оператор startsWith принимает как второй параметр не только строку, но и массив значений и таких примеров много. Следующий пример полностью равнозначен ранее приведенному:
  1. jLinq.from(users) 
  2.   .startsWith("fio",["Vas", "Pet"])
  3.   .select();
Одна из самых полезных возможностей jlinq – естественное связывание в одном объекте фильтре как простых критериев, таких как сравнения и сложных выражений, когда без пользовательской функции сравнения не обойтись (по аналогии с функцией orCombine есть функция andCombine):
  1. jLinq.from(users)  
  2.   .startsWith("fio","Vas")
  3.   .orCombine(function(rec) {
  4.       rec.less ("weight", 80)
  5.   })
  6.  .select();
Но это еще не все. Ранее я приводил пример, когда нужно выполнять поиск на сложносоставном объекте: например, у сотрудника есть адрес, состоящий из нескольких полей. Jlinq умеет понимать в условиях имена полей, содержащие перечисленные через точку компоненты. Например, так я нашел тех, сотрудников у которых в графе адреса заполнено значение поля “город”, а затем среди отобранных позиций задал дополнительный отбор тех, у кого номер дома менее чем 400.
  1. // пример данных
  2. var users = [
  3.  {fio: 'Vasya', weight: 80, adr: { city: "Minsk", street: "Petrova", home: 123 }},
  4.  ];
  5.  
  6. // пример условия поиска
  7. jLinq.from(users) 
  8.   .not()
  9.   .empty("adr.city")
  10.   .less("adr.home", 400)
  11.   .select()
Лучшим учебником по jlinq является его официальный сайт, там вы можете найти не только документацию и примеры команд, но и интерактивную “обучающую машину”. Т.е. html-страницу, где вы можете экспериментировать, вводя в текстовое поле выражения jlinq и тут же видеть результаты выполнения команд.

Как вывод: организация хранения данных в браузере уже возможна, и возможна не за счет вечно опаздывающих официальных стандартов, а за счет усилий компьютерных энтузиастов, создателей таких библиотек как persistjs, taffy и jlinq. Объединяя все эти инструменты воедино, мы получаем шанс придумать и реализовать в сети internet те продукты, которые недавно были уделом для традиционных desktop приложений. Постарайтесь не упустить момент.