Firebase: прощание с иллюзиями

Маркетинг стал частью мира разработки. По количеству звездочек на GitHub определяют, какое из похожих друг на друга решений круче, а по количеству твитов можно спрогнозировать, какая технология будет развиваться в ближайшие полгода. В таких условиях мы рискуем стать жертвами хайпа. Я — стал: моё представление о Firebase расходилось с реальностью настолько сильно, что понимание области применения технологии стало для меня настоящим откровением. Я хочу поделиться этим пониманием и тем, как всё-таки использовать Firebase правильно.

2109c84e6e1140fc8a65ccae0f5a7cb4.gif

Желание поработать с Firebase появилось у меня давно, но я ждал подходящего проекта. И дождался: MVP системы бронирования офисов. Так как это MVP, бизнес-логика бэкендa довольно примитивная. К тому же к Firebase будет подключаться мобильное приложение на iOS. С виду идеальный случай для использования сервиса, но в ходе реализации пришлось столкнуться с некоторыми проблемами, о которых и пойдёт речь дальше.

Но сначала хотелось бы устранить все недопонимания. Вот две вещи, которые нужно усвоить для работы с Firebase:

  1. это не бэкенд, а база данных. Можете забыть о тех чудо-примерах приложений на Firebase без серверной части, это пока недостижимо;
  2. это NoSQL со всеми его преимуществами и недостатками.

Выбирать решение для хранения данных нужно исходя из природы самих данных. Firebase имеет свои недостатки:
  • его область применения намного меньше, чем у NoSQL-решения;
  • Firebase сильно ограничивает вас при выборке данных и при необходимости записать данные в несколько мест одновременно;
  • далеко не со всеми структурами данных удобно работать в Firebase.

Но инструмент, позволяющий быстро начать разработку MVP и имеющий ещё массу преимуществ, выкидывать на свалку жалко. А слабые места можно и залатать, если они вам мешают.

Преамбула


Представим, что вы разрабатываете систему бронирования для сети отелей.

216ddd530d2a4771b3a85561ffbed571.png

Там есть такие сущности:

  • отель
  • номер
  • клиент
  • бронь

Как реализовать это на SQL-базе, понятно: четыре таблицы со связями, и дело в шляпе.

Как реализовать это на NoSQL (Firebase)? Можно попробовать вложить сущности в друг-друга:

{
  "отель”: {
    …
    "номера”: {
        "номер”: {
        ???
      },
      ...
    }
  }
}

Тут начинают возникать вопросы:, а стоит ли вкладывать все букинги в номер?, а куда вкладывать клиентов? И т.п. Проблема NoSQL зачастую в том, что данные приходится дублировать.

Есть второй вариант: попытаться использовать NoSQL схожим с SQL способом и создать в корне объекты для каждой сущности, а связи поддерживать, храня id других объектов.

Вероятно, в других NoSQL-базах бороться с этими проблемами проще, но решения для своих задач я в Firebase не нашёл.

Какой бы вариант вы не предпочли, у них есть одинаковая проблема: невозможность сделать сложную выборку данных. Что делать, если вы хотите получить список бронирований конкретного клиента? Эти бронирования могут оказаться вложенными в разные номера и отели, а если структура плоская, то Firebase не сможет отфильтровать данные по нескольким параметрам (эту проблему даже обсуждали на StackOverflow). В общем, если вы хотите сделать выборку по клиенту и дате бронирования, Firebase SDK вам ничем не поможет.

Можно попытаться решить эту проблему на бэкенде, но тогда вам придётся выкачивать выборку данных, отфильтрованных по одному параметру, и фильтровать её дальше самостоятельно. Это неприемлемо.

b29dc427b74344cea1e93988375d8243.png

Что делать?


Не использовать Firebase для сложной выборки данных. В этом нам может помочь собственный бэкенд на Node.js и один из нижеописанных инструментов.

ElasticSearch


96f2e782d3b844f4b82fc711d9af0da5.png

Это поисковый движок с JSON REST API, использующий Lucene и написанный на Java. Подробности можно почитать на официальном сайте, а мы сразу начнём рассматривать его в связке с Firebase.

Установка


Нужно поставить ElasticSearch на сервер (сделать это по инструкции будет несложно). После нужно интегрировать его с Firebase, а именно — создать поисковый индекс из базы Firebase. Я использовал официальную интеграцию от Firebase. Для запуска нужно скачать репозиторий, установить зависимости и заполнить config с ключами для Firebase.

В этом решении я нашел несколько минусов:

  1. это отдельное приложение на Node.js, и его сложно связать с бэкендом;
  2. создать правильный индекс для ElasticSearch непросто, и одной синхронизацией данных с базой Firebase не обойтись;
  3. типы полей присваиваются автоматически.

Прочувствуйте их на таком простом примере. У вас есть список зданий с их описанием и координатами. ElasticSearch требует, чтобы поля, отвечающие за географические координаты зданий, были объявлены таковыми заранее, на этапе создания поискового индекса. Интеграция движка с Firebase не даёт контроля над этим процессом, а просто синхронизирует все данные, автоматически определяя типы данных.

ElasticSearch бесплатен и разворачивается на своём подконтрольном сервисе — это плюс. Но вместе с тем возникает ряд проблем с деплоем и безопасностью, которые нужно продумать заранее.

Пример: открыт порт, используемый Elastic Search для внешних запросов. Это создаёт уязвимость, так как этот же порт используется для записи и управления поисковыми индексами. Возможным результатом такого недосмотра станет удаление поискового индекса или внесение в него своих данных. Поэтому изначально этот порт открыт только для запросов с той же машины, на которой установлен ElasticSearch.

Сделаем вывод: вопрос того, как реализовать взаимодействие между пользователем, ElasticSearch и бэкендом, ложится на плечи разработчика.

Algolia


def01349de7148e5a7a22e14fd8cd665.png
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 туда, где его использовать не стоит.

© Habrahabr.ru