Firebase: прощание с иллюзиями
Желание поработать с Firebase появилось у меня давно, но я ждал подходящего проекта. И дождался: MVP системы бронирования офисов. Так как это MVP, бизнес-логика бэкендa довольно примитивная. К тому же к Firebase будет подключаться мобильное приложение на iOS. С виду идеальный случай для использования сервиса, но в ходе реализации пришлось столкнуться с некоторыми проблемами, о которых и пойдёт речь дальше.
Но сначала хотелось бы устранить все недопонимания. Вот две вещи, которые нужно усвоить для работы с Firebase:
- это не бэкенд, а база данных. Можете забыть о тех чудо-примерах приложений на Firebase без серверной части, это пока недостижимо;
- это NoSQL со всеми его преимуществами и недостатками.
Выбирать решение для хранения данных нужно исходя из природы самих данных. Firebase имеет свои недостатки:
- его область применения намного меньше, чем у NoSQL-решения;
- Firebase сильно ограничивает вас при выборке данных и при необходимости записать данные в несколько мест одновременно;
- далеко не со всеми структурами данных удобно работать в Firebase.
Но инструмент, позволяющий быстро начать разработку MVP и имеющий ещё массу преимуществ, выкидывать на свалку жалко. А слабые места можно и залатать, если они вам мешают.
Преамбула
Представим, что вы разрабатываете систему бронирования для сети отелей.
Там есть такие сущности:
- отель
- номер
- клиент
- бронь
Как реализовать это на SQL-базе, понятно: четыре таблицы со связями, и дело в шляпе.
Как реализовать это на NoSQL (Firebase)? Можно попробовать вложить сущности в друг-друга:
{
"отель”: {
…
"номера”: {
"номер”: {
???
},
...
}
}
}
Тут начинают возникать вопросы:, а стоит ли вкладывать все букинги в номер?, а куда вкладывать клиентов? И т.п. Проблема NoSQL зачастую в том, что данные приходится дублировать.
Есть второй вариант: попытаться использовать NoSQL схожим с SQL способом и создать в корне объекты для каждой сущности, а связи поддерживать, храня id других объектов.
Вероятно, в других NoSQL-базах бороться с этими проблемами проще, но решения для своих задач я в Firebase не нашёл.
Какой бы вариант вы не предпочли, у них есть одинаковая проблема: невозможность сделать сложную выборку данных. Что делать, если вы хотите получить список бронирований конкретного клиента? Эти бронирования могут оказаться вложенными в разные номера и отели, а если структура плоская, то Firebase не сможет отфильтровать данные по нескольким параметрам (эту проблему даже обсуждали на StackOverflow). В общем, если вы хотите сделать выборку по клиенту и дате бронирования, Firebase SDK вам ничем не поможет.
Можно попытаться решить эту проблему на бэкенде, но тогда вам придётся выкачивать выборку данных, отфильтрованных по одному параметру, и фильтровать её дальше самостоятельно. Это неприемлемо.
Что делать?
Не использовать Firebase для сложной выборки данных. В этом нам может помочь собственный бэкенд на Node.js и один из нижеописанных инструментов.
ElasticSearch
Это поисковый движок с JSON REST API, использующий Lucene и написанный на Java. Подробности можно почитать на официальном сайте, а мы сразу начнём рассматривать его в связке с Firebase.
Установка
Нужно поставить ElasticSearch на сервер (сделать это по инструкции будет несложно). После нужно интегрировать его с Firebase, а именно — создать поисковый индекс из базы Firebase. Я использовал официальную интеграцию от Firebase. Для запуска нужно скачать репозиторий, установить зависимости и заполнить config с ключами для Firebase.
В этом решении я нашел несколько минусов:
- это отдельное приложение на Node.js, и его сложно связать с бэкендом;
- создать правильный индекс для ElasticSearch непросто, и одной синхронизацией данных с базой Firebase не обойтись;
- типы полей присваиваются автоматически.
Прочувствуйте их на таком простом примере. У вас есть список зданий с их описанием и координатами. ElasticSearch требует, чтобы поля, отвечающие за географические координаты зданий, были объявлены таковыми заранее, на этапе создания поискового индекса. Интеграция движка с Firebase не даёт контроля над этим процессом, а просто синхронизирует все данные, автоматически определяя типы данных.
ElasticSearch бесплатен и разворачивается на своём подконтрольном сервисе — это плюс. Но вместе с тем возникает ряд проблем с деплоем и безопасностью, которые нужно продумать заранее.
Пример: открыт порт, используемый Elastic Search для внешних запросов. Это создаёт уязвимость, так как этот же порт используется для записи и управления поисковыми индексами. Возможным результатом такого недосмотра станет удаление поискового индекса или внесение в него своих данных. Поэтому изначально этот порт открыт только для запросов с той же машины, на которой установлен ElasticSearch.
Сделаем вывод: вопрос того, как реализовать взаимодействие между пользователем, ElasticSearch и бэкендом, ложится на плечи разработчика.
Algolia
SaaS-решение для поиска. Платное, но с бесплатным планом. С прайсом и прочими деталями можно ознакомиться на официальном сайте.
Интеграция с Firebase реализована при помощи официальной js-библиотеки. Процесс установки и запуска подробно описан в readme, и у меня всё заработало с первой попытки.
Выглядит интеграция примерно так:
var algoliasearch = require('algoliasearch');
…
var client = algoliasearch(config.algolia.applicationID, config.algolia.apiKey); // инициализируем Algolia
var indexRooms = client.initIndex('rooms'); // инициализируем поисковый индекс Algolia
rooms.once('value', initInde); // rooms — это reference к объекту в Firebase
function initIndex(dataSnapshot) {
var objectsToIndex = []; // Этот массив мы отправим в Algolia
var values = dataSnapshot.val(); // Получаем значение snapshot’a
for (var key in values) { // обрабатываем каждый room
if (values.hasOwnProperty(key)) {
var firebaseObject = values[key];
firebaseObject.objectID = key; // id объекта в Algolia должен совпадать с ключом в Firebase
objectsToIndex.push(firebaseObject); // добавляем в массив
}
}
indexRooms.saveObjects(objectsToIndex, function(err, content) {
if (err) {
console.log('error');
return;
}
console.log('success');
return;
});
}
В результате мы получаем поисковый индекс в Algolia, содержащий все объекты rooms из Firebase. Обратите внимание, что по ходу импорта данные можно обработать дополнительно, например подтянуть название отеля из другого объекта в базе данных.
После того, как мы создали индекс, мы не собираемся обновлять его целиком, поэтому в дальнейшем следим за событиями в Firebase и обрабатываем их:
rooms.on('child_added', addOrUpdateObjectRooms);
rooms.on('child_changed', addOrUpdateObjectRooms);
rooms.on('child_removed', removeIndexRooms);
Единственный минус в использовании Algolia в том, что за SaaS нужно платить. Но для MVP бесплатного тарифа должно быть достаточно, а делать на Firebase масштабный проект мало кому придёт в голову (я надеюсь).
В противовес этому сомнительному минусу мы получаем удобную админку с доступом к аналитике, поисковому индексу и нюансам работы поисковых запросов.
Важным плюсом является наличие SDK под всё и вся — от мобильных платформ до фреймворков для бэкенда. В суть я не вникал, но iOS-разработчик сказал: это удобнее, чем REST.
Я советую вам попробовать именно Algolia: интеграция с Firebase лучше, установка проще, а в довесок мы получаем консоль с аналитикой и SDK. Я оставил без внимания технические детали и не анализировал производительность и скорость, это сложная и отдельная тема.
Итоги
Выгоды этой довольно простой системы ощутимы. Мы получаем:
- Firebase для хранения данных, всех операций чтения и простых неконкурентных запросов;
- Node.js для всех конкурентных запросов и сложной бизнес-логики + обслуживания Algolia/ElasticSearch;
- Algolia/ElasticSearch для поиска и сложной выборки данных.
Налицо все преимущества Firebase, без недостатков в виде необходимости дублировать данные или организовывать сложные и медленные выборки на Node.js. При таком раскладе можно легко реализовать систему бронирования или любую другую задачу, требующую транзакций и сложных выборок данных. Например можно сначала подобрать комнату на конкретный день на двух человек, с кондиционером и балконом, а потом забронировать её и не бояться, что комната уже была занята или будет занята повторно. Некоторые данные, правда, придётся дублировать, но исключительно в самом поисковом индексе, а не базе данных.
При условии грамотного использования Firebase становится вполне приемлемым решением для доступа и хранения данных. Всегда помните, что данные первичны, и если вы выбрали неправильную структуру данных или способ работы с ними, вас ждут серьёзные проблемы в разработке.
Жду в комментариях ваших историй об интеграции Firebase и замечаний по статье. Спасибо!
Комментарии (3)
24 января 2017 в 15:10
0↑
↓
Я недавно начал работать с Firebase (пробую писать кой-чего под Android). Мне в нем нравится то, что для моего приложения не нужно писать вообще ни байта серверного кода (за исключением правил для базы данных). Но огорчает ужаснейшая документация и отсутствие возможности сделать выборку элементов из базы по идентификаторам.
Кстати, вложенные сущности использовать наоборот не рекомендуется. Суть в том, что при подписке на какой-либо из ключей при изменении вложенных сущностей передаются вообще все данные этого самого ключа. Например, в отеле изменили данные о номере. Если вы были подписаны на »/отель/», то вам придут данные об этом отеле целиком, со всеми номерами и прочим. Если же внутри отеля хранить данные о бронях, то придут и брони. Короче говоря, трафик увеличивается в разы. Поэтому нужно хранить данные как-то так:{ "отели": { "id отеля": { ... } }, "номера_отеля": { "id отеля": { "id номера": { ... } } } }
Сумбурно описал, но надеюсь, смысл понятен будет.24 января 2017 в 15:25
+1↑
↓
Эмуляция реляционной БД?
24 января 2017 в 15:27
0↑
↓
Придумать себе проблему, а потом ее героически преодолевать.Вывод — не надо пихать NoSQL туда, где его использовать не стоит.