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

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

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

Домашний сайт библиотеки taffy - сайт . Там вы можете скачать архив с самой библиотекой (размер всего 10 килобайт), а на странице сайт увидеть краткую справку с перечислением основных возможностей taffydb. Taffy – молодой проект: первая версия (1.0) появилась еще весной 2008 г, а за прошедшее время счетчик версий успел дойти до цифры 1.7. Хотя последние версии носят в основном характер “исправления ошибок” и новая функциональность не добавляется. Итак, после того как вы скачали библиотеку, разархивировали и подключили ее к вашему html-файлу, самое время создать “Коллекцию объектов”. Taffy, по своей сути, всего лишь набор функций, которые выполняют работу над набором объектов-записей. Каждая запись представляет собой набор пар: название характеристики и ее значение. Вот пример массива записей с информацией о людях:
var users = [
{fio: 'Vasya', weight: 80},
{fio: 'Petya', weight: 60}
];

Такой массив данных вряд ли будет присутствовать на html-странице – более вероятен сценарий, когда с сервера будет загружена текстовая строка с JSON представлением массива. А значит, нам нужен способ выполнить преобразование массива в строку JSON и обратно. Почти все javascript библиотеки имеют подобную функциональность, например, библиотека Yahoo UI, о которой я недавно закончил серию статей, содержит функции parse и stringify:
// преобразуем строку в JSON-объект
var json = YAHOO.lang.JSON.parse (oResponse.responseText);
// и обратное преобразование объекта в строку
alert (YAHOO.lang.JSON.stringify(json));

К счастью, нет никакой необходимости “тащить” в проект еще какую-нибудь javascript-библиотеку ради пары функций, так как в состав taffy входит функции работы с JSON:
// так я преобразую массив объектов в строку
var s = TAFFY.JSON.stringify(users);
// а теперь преобразуем строку в набор объектов
alert ( TAFFY.JSON.parse(s) );

Теперь вернемся к задаче, вынесенной в заголовок статьи, – поиску, фильтрации, сортировке данных. Для этого я создаю объект taffy, передав на вход конструктору либо
JSON массив объектов, либо строку:
var t = new TAFFY(users);

Теперь от имени переменной “t” я могу вызывать функцию find, передав ей в качестве параметра специальный объект – шаблон поиска. Так, в следующем примере я найду всех людей, вес которых равен 50, 60 и 70 кг:
t.find({weight: [50, 60, 70]});

Здесь и далее, когда мы записываем условие, то оно помещается внутрь фигурных скобок, то есть условие – это JSON-объект. Полями являются названия полей записей в массиве (“fio” или “weight”), по которым идет поиск. А значение поля кодирует накладываемое на поле условие сравнения. Результатом вызова метода find будет массив с порядковыми номерами записей, которые подошли под условие, то есть номера 1 и 2. Имея в своем распоряжении порядковые номера записей, добраться до их содержимого проще простого, однако если вы хотите сразу получить не номера объектов, а их значения, то используйте вызов метода get:
t.get({weight: [50, 60, 70]});

В случае если вас интересует не весь список, а только первый или последний из найденных элементов, то можно сделать так:
t.first({weight: [50, 60, 70]});
//или если нужен последний элемент:
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 позволяет записывать фильтры, в которых есть не только простейшее сравнение на равенство или неравенство, но и операции сравнения. Так, в следующих примерах показываются все простые операторы.
t.find({weight: {equal : 50} })
t.find({weight: {is: 50} })

Equal и is являются синонимами и служат для того, чтобы задать точное условие на равенство. Для отношений больше-меньше мы используем операторы: “greaterthan” и или сокращение “gt” (“больше чем”):
t.find({weight: {greaterthan: 50} })

По аналогии используется оператор сравнения “меньше чем” (lessthan или lt):
t.find({weight: {lt: 50} })

В том случае, если вы хотите записать обратное условие, например, найти всех людей, у которых вес не равен 50 кг, то нужно записать перед оператором сравнения символ знака восклицания, то есть “!is” означает “не равно” (также знак “!” можно комбинировать и с операторами “lt” и “gt”):
t.find({weight: {"!is": 50} })

Для работы со строками оператора сравнения “is” мало – нужны средства сравнения строк по шаблону или по регулярному выражению. И в taffy данная функциональность есть. Оператор “starts” или “startswith” помогут найти всех людей, имена которых начинаются с букв “Pet”:
t.find({fio: {"startswith": "Pet" } })

Если же мы хотим найти тех, чья фамилия заканчивается на буквы “rov”, то используем оператор “endswith” или “ends”. Если стоит задача поиска в ФИО определенного буквосочетания, но при этом нет разницы, будет ли оно находиться в начале, в середине или конце строки, то используем оператор “like”. К сожалению, в отличие от оператора like из мира баз данных и SQL, мы не можем использовать специальные символы подстановки “?”, “*” (“_” и “%”):
t.find({fio: {like: "Pet" } })

Раз функция like не поможет нам в записи сложных шаблонов соответствия строк, то остается только воспользоваться регулярными выражениями:
t.find ({fio: {regex: /vasya/i} })

Так, в показанном примере я нашел всех сотрудников, в ФИО которых присутствовало слово “vasya” без учета регистра. Запись /vasya/i – является стандартным для javascript способом создания Regexp (и taffy не привносит здесь ничего от себя).

Так как taffy предоставляет функции поиска только над одним единственным массивом записей; и мы не можем хранить данные в нескольких связанных между собой таблицах, то приходится создавать такие объекты-записи, что некоторые из их полей не строки, числа или даты, а снова массивы или объекты. Например, для каждого из сотрудников из предыдущего примера я хочу добавить сведения о том, какие фрукты он любит:
var users = [
{fio: 'Vasya', weight: 80, likes: ["Apple", "Orange", "Grapes"] } ];

Теперь, если я хочу найти тех сотрудников, которые предпочитают яблоки, то нужно записать запрос поиска так:
t.find ({likes: {has: "Apple"} })

Если же мне нужно узнать, любит ли сотрудник не только яблоки, но еще апельсины и виноград, то я использую оператор сравнения hasAll:
t.find ({likes: {hasAll: ["Apple", “Orange”, “Grapes”]} })

Данные можно не только отбирать на основании условий, но и сортировать. Для этого я использую функцию orderBy, передавая ей как параметр массив имен полей, по которым нужно выполнить сортировку. После того как коллекция taffy была отсортирована, можно применять к ней условия поиска. t.orderBy ([“weight”, “fio”])
// а так можно поменять направление сортировки на обратное
t.orderBy ([weight: “desc”])
// и выведем всю отсортированную коллекцию на экран
alert ( t.get() );

В тех случаях, когда условие сортировки сложное и его нельзя записать просто как перечень имен полей, то можно как параметр вызова метода orderBy передать собственную функцию сравнения, например, так:
function mySort (a, b){
if (a.fio < b.fio) return +1;
if (a.fio > b.fio) return -1;
return 0;
}
t2.orderBy(mySort);

Теперь рассмотрим примеры того, как можно изменять информацию внутри коллекции taffy. Так, для удаления записей по критерию используется функция remove, ее параметры задаются по точно таким же правилам, как и условия поиска для функций get и find:
t.remove ({weight: 50})

Для того чтобы вставить новую запись, мы используем функцию insert, в качестве параметра для которой передается или отдельная запись, или их массив. В любом случае новые записи будут добавлены в конец существующей коллекции:
t.insert ({fio: 'Ronald', weight: 80})

Редактирование записей также построено на идее объектов-шаблонов, как для условия поиска, так и для правил редактирования. Так, в следующем примере я установлю новое значение веса Ronald-а:
t.update ({weight: 100}, {fio: “Ronald”})

Все описанные выше примеры были очень простые и удобно записывались с помощью taffy фильтров, но как только сложность поисковых запросов растет, картина резко ухудшается. Прежде всего, неудобно записывать условия поиска, в которых на одно поле накладывается несколько условий, например, мы хотим найти всех людей, у которых вес менее 100 кг, но не равен при этом 50 кг. Первым шагом я нашел всех людей, вес которых менее 100 кг, а затем полученный список порядковых номеров был подан второй раз на вход функции find:
var found1 = t.find ( {weight: {lt: 100} } );
// ограничиваем поиск теми, кого мы нашли в прошлый раз
var found2 = t.find ({ weight: {"!is": 50}}, found1)
// и выводим найденные записи на экран
alert (t.get(found2));

Если вам нужно записать несколько условий через оператор OR (или), то опять-таки придется комбинировать несколько вызовов метода find, не забыв при этом избавиться от возможных дубликатов:
var found1 = t.find ({ weight: {"lt": 50}})
var found2 = t.find ({ fio: “Vasya”})
alert (TAFFY.gatherUniques (found1.concat(found2)));

Что более неприятно, taffy не умеет корректно обрабатывать условия поиска по составным объектам. К примеру, у каждого из сотрудников есть адрес, состоящий из города и номера дома. Нам нужно найти всех людей, живущих в г.Минске. К сожалению, нельзя записать что-то вроде:
t.find ({“address.city”: “minsk”})

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

Домашний сайт проекта Jlinq - сайт . Размеры библиотек jlinq и taffy практически одинаковы: и там и там 10 кБ в сжатом виде. Вот только возможностей у jlinq побольше за счет того, что она не содержит функций по конвертации данных в JSON формат. Итак, я предполагаю, что вы загрузили библиотеку и подключили ее к своему html-файлу. В качестве примера данных я воспользуюсь тем же массивом записей о сотрудниках, что и в примере с taffy. Предполагая, что в переменной users хранятся данные, я могу записать так:
var results = jLinq.from(users)
.startsWith("fio", "Vas")
.or("Pet")
.orderBy("weight")
.select();

Этот пример кода демонстрирует общую методику работы с jlinq: все выполняемое выражение состоит из четырех последовательных частей. Во-первых, нужно указать на источник записей, за это отвечает конструкция jLinq.from. Потом идет произвольное количество условий отбора (вторая часть запроса – это команды отбора), связанные между собой логическими операторами (третья часть — это ключевые слова and, or, not). Четвертая часть выражения jlinq служит для указания того, что нужно сделать. В примере слово select говорит, что нужно найти все записи, соответствующие записанному условию отбора. Кроме select еще есть first и last (для получения первой и последней записи, подошедшей по условию). Оператор orderBy позволяет записать условия сортировки. В случае если мы хотим задать сортировку по какому-то из полей в обратном порядке, то перед именем поля ставится знак “-”:
.orderBy("-weight", "fio")

Еще пример часто используемого оператора – это count, он вернет количество найденных записей. Если мы хотим избавиться от дублирующихся записей, то поможет команда distinct. Так, в следующем примере я получил массив с уникальными значениями веса людей.
jLinq.from(users)
.distinct("weight");

Каждое условие отбора данных записывается в виде функции или оператора сравнения, первым параметром для которого идет имя поля, а вторым – значение, с которым нужно выполнить сравнение. Также как и для taffy, если мы записываем последовательно несколько операторов, например, startsWith, затем endsWith, то эти операторы связываются между собой оператором AND. Вызов функции and() может быть либо явным, как в следующем примере, либо предполагаться по умолчанию:
jLinq.from(users)
.startsWith("fio", "Vas")
.and()
.greater("weight", 200)
.orderBy("weight")
.select();

Если же мы хотим перечислить условия через ИЛИ (OR), то можно, например, записать условие так:
jLinq.from(users)
.startsWith("fio","Vas")
.or()
.startsWith("fio","Pet")
.select();

Но так как это выглядит несколько громоздко, то jlinq представляет функцию повторения выражения. Так, когда я записываю оператор startsWith, то он сохраняется вместе со своим первым аргументом – именем поля. И в дальнейшем я могу записать внутри оператора “or” всего одно значение, то есть то, с чем нужно выполнить сравнение.
jLinq.from(users)
.startsWith("fio","Vas")
.or("Pet")
.select();

К слову сказать, приятным отличием jlinq от taffy является гибкость многих стандартных операторов сравнения. Так, оператор startsWith принимает как второй параметр не только строку, но и массив значений, и таких примеров много. Следующий пример полностью равнозначен ранее приведенному:
jLinq.from(users)
.startsWith("fio",["Vas", "Pet"])
.select();

Одна из самых полезных возможностей jlinq – естественное связывание в одном объекте фильтре как простых критериев, таких как сравнения, так и сложных выражений, когда без пользовательской функции сравнения не обойтись (по аналогии с функцией orCombine есть функция andCombine):
jLinq.from(users)
.startsWith("fio","Vas")
.orCombine(function(rec) {
rec.less ("weight", 80)
})
.select();

Но это еще не все. Ранее я приводил пример, когда нужно выполнять поиск на сложносоставном объекте: например, у сотрудника есть адрес, состоящий из нескольких полей. Jlinq умеет понимать в условиях имена полей, содержащие перечисленные через точку компоненты. Например, так я нашел тех сотрудников, у которых в графе адреса заполнено значение поля “город”, а затем среди отобранных позиций задал дополнительный отбор тех, у кого номер дома менее чем 400.
// пример данных
var users = [
{fio: 'Vasya', weight: 80, adr: { city: "Minsk", street: "Petrova", home: 123 }},
… ]
// пример условия поиска
jLinq.from(users)
.not()
.empty("adr.city")
.less("adr.home", 400)
.select()

Лучшим учебником по jlinq является его официальный сайт, там вы можете найти не только документацию и примеры команд, но и интерактивную “обучающую машину”. То есть html-страницу, где вы можете экспериментировать, вводя в текстовое поле выражения jlinq и тут же видеть результаты выполнения команд.

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



black-zorro@tut.by black-zorro.com


Компьютерная газета. Статья была опубликована в номере 22 за 2009 год в рубрике программирование

©1997-2024 Компьютерная газета