Желание поработать с 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 и замечаний по статье. Спасибо!
Комментарии (28)
A_V_E
24.01.2017 15:27+11Придумать себе проблему, а потом ее героически преодолевать.
Вывод — не надо пихать NoSQL туда, где его использовать не стоит.DmitrySkripkin
24.01.2017 15:33+1Вывод верный, без костылей на Firebase далеко не уехать. Однако есть и свои плюсы. Авторизация и роли, real-time, SDK под разные платформы и многие другие полезные фичи. Для MVP годится.
A_V_E
24.01.2017 15:38+3Просто изначально Ваша задача подразумевала реляционность. Использовать для нее нереляционную БД — это напрашиваться на усложнение архитектуры. В частности, неизбежное потом прикручивание поисковика — эластика/солр/люцены/etc, просто потому, что понадобилось добавить фичу чуть-чуть усложненного поиска — и тут noSQL ожидаемо ломается.
justboris
24.01.2017 16:03Firebase позиционируется как Backend as a Service. NoSQL — это детали реализации, на которые наступаешь только уже попробовав.
Посоветуете аналогичный сервис, только с SQL структорой данных?
A_V_E
24.01.2017 18:03В Backendless есть relations:
https://backendless.com/documentation/data/js/data_relations.htm
Плюс там есть поддержка внешних бд (поддерживаются пока только MySQL/MariaDB).
GennPen
24.01.2017 15:40+9Имейте совесть, не ставьте в следующий раз на КДПВ 10 мегабайтную гифку.
justboris
24.01.2017 16:05+1+1, не надо так, пожалуйста. Пара таких статей в ленте RSS — и квоте мобильного трафика на день придет конец.
Kanumowa
25.01.2017 12:16В настройках мобильного приложения можно включить загрузку картинок только через WiFi
LoadRunner
26.01.2017 17:19+1А можно и не включать. На десктопах 10-мегабайтные гифки тоже не вызывают фонтанов радости. На слабых компьютерах рендеринг повесит систему наглухо.
ostapbender
24.01.2017 15:51+17Хипстеры на марше.
Ради задачи, для решения которой (в виде MVP) за глаза хватит SQLite, притащили адовый ворох SaaS решений, потратили кучу времени на взаимное интегрирование всего барахла (без описания, конечно же, что, куда и с какими учетными данными за чем обращается), технично разложили грабли с денормализацией и дублированием данных, да помножили в своей этой Ноде все строки на картинки.
Оглушительный успех, я считаю.
i360u
25.01.2017 12:09+1Firebase — это, какбэ, далеко не только БД. А реализация собственной полноценной архитектуры/инфраструктуры дает не меньший простор для "ломания дров". Все вокруг такие умные, хипстерами обзываются, а сами никогда не лажают, ага.
zharikovpro
24.01.2017 17:33+1> позволяющий быстро начать разработку MVP
На многие грабли не пришлось бы наступать, если изначально продумали бы как быстро завершить разработку.
vintage
25.01.2017 00:31OrientDB:
- гибридная субд, хранящая грубо говоря JSON-ы с отношениями.
- умеющая в права пользователей вплоть до права конкретному пользователю на чтение конкретной записи.
- Поддежка REST и адаптеры под разные языки.
- Lucene на равне с другими индексами, из коробки работает по любым полям.
- Все данные хранятся в нормализованном виде.
- Шардинг и репликация из коробки.
- Веб админка тоже в коробке.
- Простым запросом можно выбрать всё необходимое на нужный уровень глубины связей.
- Может встраиваться в Java приложение.
Подробнее тут: https://habrahabr.ru/post/267079/
Riim
25.01.2017 16:37С ArangoDB не приходилось сравнивать? Сейчас между ними выбираю.
vintage
25.01.2017 22:19+1http://db-engines.com/en/system/ArangoDB%3BOrientDB%3BNeo4j
http://vschart.com/compare/arangodb/vs/orientdb
Фичи, которые есть в Orient, но нет в Arango:
- Автоматические распределённые запросы. Ориент знает на какой ноде что лежит и сам делает map-reduce.
- Прямые ссылки между документами. Это даёт простые запросы, быстрый переход по связям, удобное администрирование.
- Возможность встраивания в Java приложение с доступом к низкоуровневым API.
- Поддержка пессимистичных и оптимистичных блокировок.
- Триггеры, правда мне в своё время не удалось прикрутить их ко внешнему приложению.
- Fetch plan, позволяющий выбирать не только найденные узлы, но и связанные с ними по заданным правилам.
- Более развитая поддержка графов.
- Продвинутая система прав: пользователи/роли, записи/классы, чтение/запись/обновление/удаление, наследование.
- Шифрованное хранилище.
Преимущества Arango:
- Встроенный NodeJS со всеми вытекающими.
dim_s
25.01.2017 10:40ElasticSearch прекрасно может жить без Firebase как поисковой движок + база данных NoSQL.
iloveip
25.01.2017 11:14-1Мы на нашем сайте тоже используем Firebase для сервиса Банки http://www.iloveip.ru/banki/ и Algolia для поиска. Firebase работает в связке с таблицей Google, я писала об этом подробно на Хабре (https://m.habrahabr.ru/company/iloveip/blog/312232/). Для MVP Firebase действительно подходит, но в будущем мы будем от него отказываться. А вот к Algolia нет никаких вопросов, для поиска на статичном сайте этот сервис вполне устраивает.
iloveip
25.01.2017 11:35-1Прошу прощения, отправляла комментарий с мобильного, поэтому ссылки не поставились.
Вот пост на Хабре про использование Firebase https://habrahabr.ru/company/iloveip/blog/312232/
И ссылка на сервис http://www.iloveip.ru/banki/
i360u
25.01.2017 11:44ИМХО, для MVP вполне подходит вариант хранения связей по идентификаторам в сущностях. Проблемы возникают только если формат данных подразумевает большое количество связей и тогда это вопрос правильной категоризации. Firebase — это отличный инструмент, который позволяет реально быстро стартануть и многое дает из коробки (включая хостинг статики), но конечно подходит не на все случаи жизни. Не думаю, что его стоит в этом винить.
via-site
29.01.2017 23:23Последние несколько месяцев разрабатываю «Offline First Progressive Web App» (для работы с очень плохим или вообще без Интернета). С самого начала планировал использовать Firebase для быстрого старта.
Проблема возникла уже в самом начале, т.к. браузерный клиент Firebase не поддерживает полноценную офлайн работу. Лучшим решением что я смог найти оказался как бы порт CouchDB в браузеры — PouchDB и во всех современных (и не очень) браузерах он работает просто великолепно. Из коробки есть встроенная, автоматическая синхронизация с CouchDB (пока не проверял) или DBaaS сервисами типа: https://console.ng.bluemix.net/catalog/services/cloudant-nosql-db
Вот сейчас сижу и думаю: «зачем мне вообще использовать Firebase, если PouchDB — Simple Server — CouchDB / DBaaS CouchDB решают все мои проблемы?»
ertaquo
Я недавно начал работать с Firebase (пробую писать кой-чего под Android). Мне в нем нравится то, что для моего приложения не нужно писать вообще ни байта серверного кода (за исключением правил для базы данных). Но огорчает ужаснейшая документация и отсутствие возможности сделать выборку элементов из базы по идентификаторам.
Кстати, вложенные сущности использовать наоборот не рекомендуется. Суть в том, что при подписке на какой-либо из ключей при изменении вложенных сущностей передаются вообще все данные этого самого ключа. Например, в отеле изменили данные о номере. Если вы были подписаны на "/отель/", то вам придут данные об этом отеле целиком, со всеми номерами и прочим. Если же внутри отеля хранить данные о бронях, то придут и брони. Короче говоря, трафик увеличивается в разы. Поэтому нужно хранить данные как-то так:
Сумбурно описал, но надеюсь, смысл понятен будет.
AllexIn
Эмуляция реляционной БД?
ertaquo
Ага. Вот из доков кусок: https://firebase.google.com/docs/database/web/structure-data#flatten_data_structures
И подобный стиль работы используется и в официальных примерах.
DrPass
Да дело в том, что это просто неподходящий инструмент для такой задачи. Если ваши данные имеют реляционную структуру, то и голову ломать не нужно, вам требуется реляционная СУБД. А подобные варианты — это забивание гвоздей крестообразной отвёрткой и закручивание шурупов молотком.