
Что делать, если нужно хранить разнообразные данные децентрализованно? Объекты, массивы, даты, числа, строки, да что угодно. Обязательно ли придумывать мощную СУБД для этого? Ведь часто нам просто нужно хранить и получать данные распределенно, открыто, но максимально просто и без особых притязаний.
В этой статье хотел бы немного раскрыть библиотеку metastocle, с помощью которой можно решить вышеописанную задачу легко, но с некоторыми ограничениями.
Немного предыстории
Где-то год назад, появилась желание и необходимость создать музыкальное хранилище. В этой статье подробности. С самого же начала было понятно, что нужно все написать так, чтобы можно было в будущем сделать то же самое и с другими сущностями: книги, видео, и.т.д. Было решено разделить все на слои, которые можно использовать независимо.
Metastocle — один из слоев, позволяющий хранить и получать много типов данных (но не файлы), в противовес слою storacle, который реализует работу именно с файлами.
Сохраняя файлы, нам нужно куда-то записать хэши, чтобы потом иметь к ним доступ. Как раз для этого нам и пригодился metastocle. В нем мы храним все что нужно: названия песен, ссылки на файлы и.т.д.
В итоге, все это было приведено к некому универсальному виду, и система состоит из трех основных сущностей:
- Коллекции — аналогично популярным nosql базам данных, сущность для определения структуры данных, различных опций и.т.п.
- Документы — непосредственно сами данные, в виде объектов.
- Действия (инструкции) — набор правил для обработки требуемых данных: фильтрация, сортировка, лимитирование и.т.д.
Давайте посмотрим пару примеров:
Сервер:
const Node = require('metastocle').Node; (async () => { try { const node = new Node({ port: 4000, hostname: 'localhost' }); // Создаем коллекцию await node.addCollection('test', { limit: 10000, pk: 'id' }); await node.init(); } catch(err) { console.error(err.stack); process.exit(1); } })();
Клиент:
const Client = require('metastocle').Client; (async () => { try { const client = new Client({ address: 'localhost:4000' }); await client.init(); // Добавляем документ const doc = await client.addDocument('test', { text: 'hi' }); // Обновляем этот документ await client.updateDocuments('test', { text: 'bye' }, { filter: { id: doc.id } }); // Добавляем еще один документ await client.addDocument('test', { id: 2, text: 'new' }); // Получаем второй документ const results = await client.getDocuments('test', { filter: { id: 2 } }); // Получаем его иначе const doc2 = await client.getDocumentById('test', 2)); // Добавляем еще документов for(let i = 10; i <= 20; i++) { await client.addDocument('test', { id: i, x: i }); } // Получаем документы, соответствующие всем условиям const results2 = await client.getDocuments('test', { filter: { id: { $gt: 15 } }, sort: [['x', 'desc']], limit: 2, offset: 1, fields: ['id'] }); // Удаляем документы, у которых id > 15 await client.deleteDocuments('test', { filter: { id: { $gt: 15 } } }); } catch(err) { console.error(err.stack); process.exit(1); } })();
Клиенты не могут создавать коллекции, сеть сама устанавливает структуру, а пользователи лишь работают с документами. Коллекции можно описывать и декларативно, через опции узла:
const node = new Node({ port: 4000, hostname: 'localhost', collections: { test: { limit: 10000, pk: 'id' } } });
Основные настройки коллекции:
- pk — поле первичного ключа. Можно не указывать, если таковое не требуется. Если поле указано, то, по умолчанию, создается uuid хэш, но при желании можно прокинуть любую строку или число.
- limit — максимальное количество документов на одном узле
- maxSize — максимальная память на данную коллекцию
- queue — режим очереди: если включен, то при достижении лимита определенные документы удаляются, чтобы записать новые
- limitationOrder — если лимитирование и очередь включены, то можно указать правила сортировки для определения удаляемых документов. По умолчанию, удаляются те, с которыми уже давно не работали.
- schema — структура полей документов
- defaults — значения по умолчанию полей документов.
- setters — хуки полей документов при сохранении.
- getters — хуки полей документов при получении.
- preferredDuplicates — можно указать предпочтительное количество дубликатов документа в сети.
Структура полей коллекции (schema) может быть описана в виде:
{ type: 'object', props: { count: 'number', title: 'string', description: { type: 'string' }, priority: { type: 'number', value: val => val >= -1 && val <= 1 }, goods: { type: 'array', items: { type: 'object', props: { title: 'string', isAble: 'boolean' } } } } }
Полный набор всех правил можно найти в функции utils.validateSchema() в https://github.com/ortexx/spreadable/blob/master/src/utils.js
Значения по умолчанию и хуки могут быть в виде:
{ defaults: { date: Date.now priority: 0 'nested.prop': (key, doc) => Date.now() - doc.date }, setters: { priority: (val, key, doc, prevDoc) => prevDoc? prevDoc.priority + 1: val }, getters: { priority: (val, key, doc) => val - 1 } }
Основные особенности библиотеки:
- Работа по принципу CRUD
- Хранение всех типов данных Javascript, которые можно сериализовать, в том числе вложенных.
- Данные могут добавляться в хранилище через любой узел
- Данные могут дублироваться для большей надежности
- Запросы могут содержать вложенные фильтры
Изоморфность
Клиент написан на javascript и изоморфен, его можно использовать прямо из браузера.
Можно загрузить файл https://github.com/ortexx/metastocle/blob/master/dist/metastocle.client.jsкак скрипт и получить доступ к window.ClientMetastocle либо импортить через систему сборки и.т.п.
Api клиента
- async Client.prototype.addDocument() — добавление документа в коллекцию
- async Client.prototype.getDocuments() — получение документов из коллекции по каким-либо инструкциям
- async Client.prototype.getDocumentsСount() — получение количества документов в коллекции
- async Client.prototype.getDocumentByPk() — получение документа из коллекции по первичному ключу
- async Client.prototype.updateDocuments() — обновление документов в коллекции по каким-либо инструкциям
- async Client.prototype.deleteDocuments() — удаление документов из коллекции по каким-либо инструкциям
Основные действия (инструкции):
.filter — фильтрация данных, пример:
{ a: { $lt: 1 }, $and: [ { x: 1 }, { y: { $gt: 2 } }, { $or: [ { z: 1 }, { "b.c": 2 } ] } ] }
.sort — сортировка данных, пример:
{ sort: [['x', 'asc'], ['y.z', 'desc']] }
.limit — количество данных
.offset — начальная позиция отбора данных
.fields — необходимые поля в документах
Более подробно все инструкции и возможные значения описаны в readme.
Работа через командную строку
Библиотеку можно использовать через командную строку. Для этого нужно установить ее глобально: npm i -g metastocle --unsafe-perm=true --allow-root. После этого можно запускать нужные экшены из директории с проектом, где узел.
Например, metastocle -a getDocumentByPk -o test -p 1 -c ./config.js, чтобы получить документ с первичным ключом 1 из коллекции test. Все экшены можно найти в https://github.com/ortexx/metastocle/blob/master/bin/actions.js
Ограничения
- Все данные сначала хранятся в памяти, а позже записываются в файл, с определенными интервалами, и при выходе из процесса. Поэтому, во-первых, нужно иметь достаточное количество оперативки, а во-вторых, иметь в виду, что запускать несколько процессов для работы с одной базой не получится.
- Шардинг на уровне всей сети реализован пока не очень эффективно. Приоритет отдается дупликации, из-за того, что размер сети нестабилен: узлы могут в любой момент отключаться, подключаться и.т.д. Поэтому если вы хотите получать из сети большое количество данных, то держите в голове, что все это будет собираться через http протокол, без особой оптимизации.
К выбору стека и этим ограничениям я пришел сознательно, поскольку цели и возможности создавать полноценную СУБД не было.
Хоть библиотека пока сыровата в плане оптимизации запросов на получение данных, но если придерживаться определенных правил, то особых проблем нет:
- Нужно максимально сужать выборку получаемых данных, стараться организовывать все так, чтобы получать документы по ключам, либо по каким-то другим полям, но отфильтрованным до оптимальных размеров.
- Если же данных все-таки надо тянуть много, то придется лимитировать каждый сервер, исходя их оптимальных размеров, для передачи их по сети. Скажем, если 10000 документов в какой-то коллекции весят 100 кб в сжатом виде, то, ограничив коллекцию на каждом узле таким значением, мы будем получать все с приемлемой скоростью.
Если создавать децентрализованный проект, в котором нужно хранить разнообразные данные, то вариантов, на данный момент, очень мало. А таких, чтобы все было удобно и доступно для среднестатистического программиста, еще меньше.
По любым вопросам обращайтесь:
