Моделируем 4 вида NoSQL-баз с помощью GlobalsDB
Перед тем как мы начнём моделировать различные виды NoSQL-баз, давайте взглянем на глобалы чуть более детатально и определим некоторые термины, которые будем использовать позднее.
При сохранении данных в элементе глобала используются 3 компонента:
- Имя глобала
- Индексы (ноль, один или несколько). Они могут быть текстовыми или численными.
- Значение (которое, собственно, и хранится в элементе глобала). Оно м.б. текстовым или численным
Эти три компонента часто записываются как N-арная реляционная переменная следующим образом:
globalName[subscript1, subscript2, ..subscriptN] = value
Это комбинация имени, индексов и значения известна как элемент глобала (Global Node) и является единицей хранения. Глобал состоит из множества его элементов, а база данных состоит из множества глобалов.
Важным свойством глобалов является то, что одиночный глобал может содержать узлы с различным числом индексов, например:
myGlobal["a"] = 123
myGlobal["b", "c1"] = "foo"
myGlobal["b", "c2"] = "foo2"
myGlobal["d", "e1", "f1"] = "bar1"
myGlobal["d", "e1", "f2"] = "bar2"
myGlobal["d", "e2", "f1"] = "bar1"
myGlobal["d", "e2", "f2"] = "bar2"
myGlobal["d", "e2", "f3"] = "bar3"
В конечном счёте одиночный глобал представляет собой разреженное иерархическое дерево элементов. Например, глобал приведённый выше описывает следующее иерархическое дерево:
Вы можете создать сколько угодно глобалов с разными именами. Другими словами, база данных в GlobalsDB состоит из одного или нескольких глобалов, каждый из которых представляет иерахию элементов.
В такой базе нет явных связей между различными глобалами, но могут быть скрытые связи, которые определяются и обслуживаются на уровне приложения.
Также в рамках БД нет явной схемы или структуры данных, которые хранятся в глобалах. Способ хранения данных в глобалах определяется на уровне приложения.
Элементы в глобалах создаются командой set. Точный синтаксис этой команды зависит от используемого API.
Так, для Node.JS API мы можем двумя способами создавать элементы один и тот же элемент глобала.
Асинхронно:
var globalNode = {
global:"myGlobal",
subscripts: ["b","c3"],
data: "Rob"
};
db.set(globalNode, function(error,results) {
// etc
});
или синхронно:
var globalNode = {
global:"myGlobal",
subscripts: ["b","c3"],
data: "Rob"
};
db.set(globalNode);
Одним из интересных свойств GlobalsDB является то, можно широко использовать синхронное программирование без вреда для производительности, что упрощает работу с ней и позволяет использовать ОО-синтаксис в полном объёме внутри JavaScript.
Это возможно из-за уникальной производительности GlobalsDB: она работает в связке c NodeJS как подлинкованный процесс в оперативной памяти (в отличие от многих других NoSQL-баз, которые работают через различные сетевые сокеты), и в сочетании с глубокой оптимизацией, которая, как уже было озвучено, даёт производительность как у баз в памяти в RAM. Подобного сочетания свойств нет у самых популярных NoSQL-баз.
Примечание переводчика: Однако если нужна именно сетевая NoSQL-база, то сделать её на том же Node.JS + GlobalsDB нетрудно — достаточно написать API на основе JSON, например.
Вызов вышеописанной команды приведёт к вставке элемента в нашу иерархию и дерево приобретёт новый вид:
Давайте теперь, помня основные свойства глобалов, посмотрим как мы можем их использовать для представления типовых структур данных, для хранения которых используются NoSQL-базы.
1) Хранилище Ключ/Значение
Реализация хранилища типа Ключ/Хначение на глобалах элементарна. Мы создаём его используя следующую структуру:
keyValueStore[key] = value
Например:
telephone["211-555-9012"] = "James, George"
telephone["617-555-1414"] = "Tweed, Rob"
В виде иерархического дерева эта структура выглядит так:
Всё, хранилище типа Ключ/Значение реализовано. Однако с помощью глобалов мы можем пойти дальше и сохранять несколько аттрибутов для одного ключа. Например, так:
Telephone[phoneNumber, "name"] == value
Telephone[phoneNumber, "address"] == value
Пример с конкретными данными:
telephone["211-555-9012", "name"] = "James, George"
telephone["211-555-9012", "address"] = "5308, 12th Avenue, Brooklyn"
telephone["617-555-1414", "name"] = "Tweed, Rob"
telephone["617-555-1414", "address"] = "112 Beacon Street, Boston"
Мы создали иерархическое дерево, которое выглядит так:
Вот код на Node.JS API для создания первой записи в этом улучшенном хранилище типа ключ/значение:
var gnode = {
global: "telephone",
subscripts: ["617-555-1414", "name"],
data: "Tweed, Rob"
};
db.set(gnode);
NoSQL-базы обычно не представляют автоматических способов для индексирования данных. Однако в БД на глобалах, если вам нужен доступ к данным по альтернативному ключу, то достаточно создать второй глобал, в котором в качестве ключа может выступать любое поле.
Например, если нам нужен доступ к данным по полю name, мы должны создать индексный глобал и обновлять его вместе с глобалом telephone. Проектирование и добавление индексов полностью лежит на вас, как разработчике, но это очень просто.
Для поддержки индекса по полю name каждый раз при добавлении записей в глобал telephone мы будем создавать элемент в глобале nameIndex:
nameIndex[name, phoneNumber] = ""
nameIndex["James, George", "211-555-9012"] = ""
nameIndex["Tweed, Rob", "617-555-1414"] = ""
Для индексных элементов в глобале не нужно сохранять значений, поэтому в качестве значения мы используем пустую строку.
На диаграмме показаны глобал с телефонными данными и индексный глобал. Пунктирной линией показаны неявные отношения между индексом и основным глобалом:
Индексный глобал позволяет нам получить доступ к данным по имени, в то время как основной глобал предоставляет доступ по телефонному номеру.
Очень важным и мощным свойством глобалов является хранение элементов в отсортированном виде (см. диаграмму ниже), все операции для этого происходят автоматически при сохранении.
Для последовательного доступа к значению каждого элемента предоставляется специальный метод-итератор для обхода глобала.
Если нам нужно создать телефонный справочник из наших данных, то мы можем последовательно обойти элементы глобала nameIndex и получить адреса из глобала telephone с помощью метода get().
Метод-итератор для обхода — это функция order. Вот пример на Node.js API:
gnode = {
global: "nameIndex",
subscripts:["James, George"]
};
var nextName = db.order(gnode).result;
Этот код должен вернуть индекс элемента, следующего за элементом с индексом «James, George» в данном глобале. То есть:
nextName = "Tweed, Rob"
GlobalsDB чрезвычайно хорошо оптимизирована для обхода индексов этим способом, так что если вы хорошо спроектируете свои индексы, то поиск данных в глобале будет исключительно быстрым.
Принципиально мы рассмотрели все пути использования глобалов в качестве простых хранилищ типа Ключ/Значение. Кстати, наше хранилище может быть перепроектировано для использования всего лишь одного глобала и для данных, и для индексов. Для этого нужно добавить ещё один индекс первого уровня, например так:
telephone["data", phoneNumber, "address"] = address
telephone["data", phoneNumber, "name"] = name
telephone["nameIndex", name, phoneNumber] = ""
Поскольку физическая реализация глобалов скрыта от нас абстрактным уровнем, мы можем создавать структуры на глобалах в точности соответствующими нашим нуждам. Однако, если хранилища типа ключ/значение начинают расти до огромных размеров, то необходимо учитывать как та или иная структура способствует или препятствует администрированию БД (резервное копирование и восстановление, максимальный размер БД, распределение между различными шард-серверами). Учёт этих факторов может повлиять на решение о том, хранить ли все данные в одном или создать несколько глобалов.
Другие типы хранилищ Ключ/Значение
Если мы взглянем на такое Ключ/Значение-хранилище как Redis, то увидим, что оно предлагает несколько других способов хранения данных. Каждый из этих способов может быть очень просто реализован на глобалах.
Списки
Списки в Redis являются связанными. Можно поместить значения в список и извлечь значения из списка, получить подсписок и т.п.
Модель подобного списка на глобалах очень проста. Например, можно использовать следующую структуру:
list[listName, "firstNode"] = nodeNo
list[listName, "lastNode"] = nodeNo
list[listName, "node", nodeNo, "value"] = value
list[listName, "node", nodeNo, "nextNode"] = nextNodeNo
list[listName, "node", nodeNo, "previousNode"] = prevNodeNo
Например, связанный список под названием myList, содержащий последовательность значений:
- Rob
- George
- John
может быть представлен как:
list["myList", "firstNode"] = 5
list["myList", "lastNode"] = 2
list["myList", "nodeCounter"] = 5
list["myList", "node", 2, "previousNode"] = 4
list["myList", "node", 2, "value"] = "John"
list["myList", "node", 4, "nextNode"] = 2
list["myList", "node", 4, "previousNode"] = 5
list["myList", "node", 4, "value"] = "George"
list["myList", "node", 5, "nextNode"] = 4
list["myList", "node", 5, "value"] = "Rob"
или графически:
На этом рисунке видна разреженная природа глобалов. Номер элемента списка — это последовательное целое число. Элемент 5 на данный момент первый элемент в списке, поэтому он имеет аттрибут nextNode в котором хранится следующий элемент списка и не имеет аттрибута previousNode для предыдущего элемента списка.
Средний элемент списка под номером 4 имеет аттрибуты для хранения номерров предыдущего и последующего элементов.
Каждая операция, которая изменяет связанный список (вставляет, извлекает, удаляет, укорачивает и т.п.), должна изменить несколько элементов внутри этого списка, например:
- сбросить указатель на первый или последний узел
- добавить или удалить значение элемента
- установить корректные значения следующего и предыдущего элементов, чтобы вставить или удалить элемент из списка
Например, чтобы вставить новое имя «Chris» в начало списка мы должны изменить глобал, где хранится список так:
list["myList", "firstNode"] = 6
list["myList", "lastNode"] = 2
list["myList", "nodeCounter"] = 6
list["myList", "node", 2, "previousNode"] = 4
list["myList", "node", 2, "value"] = "John"
list["myList", "node", 4, "nextNode"] = 2
list["myList", "node", 4, "previousNode"] = 5
list["myList", "node", 4, "value"] = "George"
list["myList", "node", 5, "nextNode"] = 4
list["myList", "node", 5, "previousNode"] = 6
list["myList", "node", 5, "value"] = "Rob"
list["myList", "node", 6, "nextNode"] = 5
list["myList", "node", 6, "value"] = "Chris"
Графическая схема изменений (то что изменилось подсвечено):
Для обхода списка мы должны начать с первого элемента и рекурсивно переходить от элемента к элементу по номеру в поле nextNode, до тех пор пока у очередного элемента мы не найдём данного поля:
Чтобы найти число элементов в списке мы можем выполнить его обход, или, для максимальной производительности, хранить это число в отдельном элементе глобала и обновлять при изменении списка:
List["myList", "count"] = noOfNodes
Понятно, что мы должны написать эти операции как методы, чтобы легко и корректно манипулировать элементами в списке, но это весьма простая задача.
Множества (Sets)
Множества в Redis это неупорядоченный набор строк. Мы можем легко смоделировать их на глобалах:
theSet[setName, elementValue] = ""
Вы можете заметить, что это в точности совпадает со способом задания индексов, рассмотренным ранее. А вот так можно добавить элемент в множество:
Set: theSet["mySet", "Rob"] = ""
Удаление элемента из множества:
Kill: theSet["mySet", "Rob"]
Для определения вхождения элемента в множество мы можем использовать команду data. Она вернёт 1, если элемент входит в множество, и 0, если нет.
Data: theSet["mySet", "Rob"] → 1
Data: theSet["mySet", "Robxxx"] → 0
В Node.js API мы можем использовать метод data
gnode = {
global: "theset",
subscripts: ["mySet", "Rob"]
};
var exists = db.data(gnode).defined;
В этом примере переменная exists получит значение 1.
Мы можем использовать упорядоченность элементов глобала, чтобы отобразить члены множества в алфавитной последовательности.
При использовании глобалов нет никакой значимой разницы при моделировании Redis-множеств set и zset.
Хэши.
Вы наверное уже заметили, что набор хэшей может быть реализован точно также как множества. В своей сущности глобалы и есть хранимые таблицы хэшей.
Hash[hashName, value] = ""
2) Табличные (или колоночные) хранилища
Табличные или колоночные NoSQL-базы такие как BigTable, Cassandra и Amazon SimpleDB позволяют сохранять данные в разреженных таблицах, подразумевая что каждая строка может содержать значения в некоторых, но необязательно во всех, столбцах.
SimpleDB в дополнение позволяет каждой ячейке в столбце содержать более одного значения.
Опять таки это означает, что такие хранилища могут быть смоделированы на глобалах. Следующая структура предоставляет базовые возможности такого хранилища:
columnStore[columnName, rowId] = value
Пример с конкретными данными:
user["dateOfBirth", 3] = "1987-01-23"
user["email", 1] = "rob@foo.com"
user["email", 2] = "george@foo.com"
user["name", 1] = "Rob"
user["name", 2] = "George"
user["name", 3] = "John"
user["state", 1] = "MA"
user["state", 2] = "NY"
user["telephone", 2] = "211-555-4121"
В виде диаграммы:
Опять нам пригодилась разреженная природа глобалов. Вышеописанный глобал представляет следующую таблицу:
name | telephone | dateOfBirth | state | ||
---|---|---|---|---|---|
1 | Rob | rob@foo.com | MA | ||
2 | George | 211-555-4121 | george@foo.com | NY | |
3 | John | 1987-01-23 |
Конечно, можно ещё добавить индексы к этой модели, например, по строке и по значению ячейки, которые нужно поддерживать одновременно с основным глобалом для хранения столбцов, например так:
userIndex["byRow", rowId, columnName] = ""
userIndex["byValue", value, columnName, rowId] = ""
3) Документо-ориентированное хранилище
Документо-ориентированные NoSQL БД такие как CouchDB и MongoDB хранят множество пар ключ/значение и вложенные множества множеств различных аттрибутов.
Обычно используются JSON или JSON-подобные структуры для представления «документов» хранящихся в этих БД. GlobalsDB автоматически отображает JSON-документы или объекты в глобалы.
Для примера рассмотрим JSON-документ:
{key:"value"}
Он может быть смоделирован на глобалах как:
Document["key"] = "value"
В GlobalsDB мы мы можем создать этот документ так:
var json = {
node: {
global: "Document",
subscripts: []
},
object: {
key: "value"
}
};
db.update(json, 'object');
И получить его из БД так:
var json = db.retrieve({global: 'Document'},"object");
console.log("Document = " + JSON.stringify(json.object));
Давайте возьмём более сложный документ:
{this:{looks:{very:"cool"}}}
Его можно представить следующим элементом глобала:
Document["this", "looks", "very"] = "cool"
А создать его можно так:
var json = {
node: {
global: "Document",
subscripts: []
},
object: {
this: {
looks: {
very: "cool"
}
}
}
};
db.update(json, "object");
А что насчёт массива?
["this","is","cool"]
Он может быть представлен так:
document[1] = "this"
document[2] = "is"
document[3] = "cool"
GlobalsDB для создания и получения отображения данных на глобалы использует объекты, а не массивы. Поэтому для сохранения массива мы напишем так:
var json = {
node: {
global: "Document",
subscripts: []
},
object: {
1: "this",
2: "is",
3: "cool"
}
};
db.update(json, "object");
Для получения массива из БД:
var json = db.retrieve({global: "Document"}, "object");
console.log("Document = " + JSON.stringify(json.object));
Document = {"1":"this", "2":"is", "3":"cool"}
Приведём более сложный JSON-документ:
{
"age": "26",
"contact": {
"address": {
"city": "Boston",
"street": "112 Beacon Street"
},
"cell": "617-555-1761",
"email": "rob@foo.com",
"telephone": "617-555-1212"
},
"knows": {
"1": "George",
"2": "John",
"3": "Chris"
},
"medications": {
"1": {
"dose": "5mg",
"drug": "Zolmitripan"
},
"2": {
"dose": "500mg",
"drug": "Paracetemol"
}
},
"name": "Rob",
"sex": "Male"
}
Он будет отображён на глобалы так:
person["age"] = 26
person["contact", "address", "city"] = "Boston"
person["contact", "address", "street"] = "112 Beacon Street"
person["contact", "cell"] = "617-555-1761"
person["contact", "eMail"] = "rob@foo.com"
person["contact", "telephone"] = "617-555-1212"
person["knows", 1] = "George"
person["knows", 2] = "John"
person["knows", 3] = "Chris"
person["medications", 1, "drug"] = "Zolmitripan"
person["medications", 1, "dose"] = "5mg"
person["medications", 2, "drug"] = "Paracetamol"
person["medications", 2, "dose"] = "500mg"
person["name"] = "Rob"
person["sex"] = "Male"
Или графически:
Мы можем создать этот документ в GlobalsDB так:
var json = {
node: {
global: "person",
subscripts: []
},
object: {
name: "Rob",
age: 26,
knows: {
1: "George",
2: "John",
3: "Chris"
},
medications: {
1: {
drug: "Zolmitripan",
dose: "5mg"
},
2: {
drug: "Paracetemol",
dose: "500mg"
}
},
contact: {
email: "rob@foo.com",
address: {
street: "112 Beacon Street",
city: "Boston"
},
telephone: "617-555-1212",
cell: "617-555-1761"
},
sex: "Male"
}
};
db.update(json, "object");
И получить так:
var json = db.retrieve({global: "person"}, "object").object;
4) Графовые базы данных
NoSQL-базы такие как Neo4j используются для представления сложных сетей взаимосвязей в терминах узлов и связей между ними (т.н. рёбер) с помощью пар ключ/значение связывающих узлы и рёбра.
Классическое использование графовой БД это представление социального графа. Давайте рассмотрим следующий пример:
В этом примере стрелками показано какие пользователи знают (know) о других. С помощью глобалов он может быть представлен так:
person[personId, "knows", personId] = ""
person[personId, "knows", personId, key) = value
person[personId, "name"] = name
Время состояния «knows» («знает») может быть вычислено из временного штампа, который сохраняется при первичном создании связи, например так:
person[1, "knows", 2] = ""
person[1, "knows", 2, "disclosure"] = "public"
person[1, "knows", 2, "timestamp"] = "2008-08-16T12:23:01Z"
person[1, "knows", 7] = ""
person[1, "name"] = "Rob"
person[2, "name"] = "John"
person[7, "knows", 2] = ""
person[7, "knows", 2, "disclosure"] = "public"
person[7, "knows", 2, "timestamp"] = "2009-12-16T10:06:44Z"
person[7, "name"] = "George"
или графически (красные точечные пунктирные линии показывают отношения «know» между пользователями в этой модели):
Если говорить об общем случае графовой БД, модель будет представлять узлы и рёбра (edge) между ними. Примерно так:
node[nodeType, nodeId] = ""
node[nodeType, nodeId, attribute] = attributeValue
edge[edgeType, fromNodeId, toNodeId] = ""
edge[edgeType, fromNodeId, toNodeId, attribute] = attributeValue
edgeReverse[edgeType, toNodeId, fromNodeId] = ""
Таким образом, разреженная природа и гибкость глобалов позволяет очень органично и просто определять сложные графовые БД.
5) Модели других баз данных
Моделирование на глобалах не ограничивается только NoSQL-моделями данных. Они также могут быть использованы для моделирования:
- XML DOM/Native XML-баз данных. GlobalsDB отлично подходит для работы с сохраняемыми XML DOM файлами. XML-документ, по существу, это граф, который представляет узлы различных типов и отношения между ними (например firstChild, lastChild, nextSibling, parent и т.п.). В сущности это позволяет GlobalsDB выступать в роли Native XML-базы данных. Модуль для Node.js ewdDOM — одна легковесных реализаций такой БД.
- Реляционные таблицы. Caché моделирует реляционные таблицы на глобалах, таким образом что можно использовать стандартные SQL-запросы. Т.е. GlobalsDB можно рассматривать как основу для NoSQL-движка, а в Caché добавлены возможности NOSQL (т.е Not-Only SQL — не только SQL) базы данных.
- Объектная БД. Caché моделирует на глобалах объекты, а также предоставляет прямое отображение между объектами и реляционными таблицами. Наверное, теперь вы понимаете как это реализовано.
В отличие от хорошо известных NoSQL-баз, GlobalsDB это не жестко-специализированная БД. Она одновременно имеет множество свойств. Так GlobalsDB может поддерживать любые из вышеописанных типов баз данных. И даже одновременно, если потребуется.
Это похоже как будто у вас есть Redis, CouchDB, SimpleDB, Neo4j и Native XML БД запущенные в одной базе данных и в одно и тоже время!
Если вы заинтересованы в NoSQL-базе работающей с Node.JS (а также .NET, Java), вам необходимо взглянуть в сторону GlobalsDB. Это воистину Универсальная NoSQL база данных!
Заключение
Конечно, способов использования глобалов значительно больше, чем приведено в данной статье. Однако, надеюсь, этот обзор продемострировал, что они удобный гибкий инструмент для абстракции и способны весьма просто моделировать различные NoSQL-базы.
Секретный соус — конечно, реализация. Если она сделана корректно, применены мудрые проектные решения, то достигнутая производительность поразительна.
Благодарность
Эта статья представляет собой адаптацию статьи Роба Твида и Джорджа Джеймса «Универсальная NoSql база данных на основе проверенной и протестированной технологии» (2010).
Автор: inetstar