dzikowski.github.io

Piszę o IT po polsku, więc z góry przepraszam za zagęszczenie kolokwializmów, ale co w angielskim brzmi naturalnie, w polskim często nie ma nawet odpowiedników.

Firebase, Realm i PouchDB

Ostatnio bawiłem się z React Native i Expo i chciałem dołączyć bazę danych, która działałaby offline. Okazało się, że nie jest to wcale takie proste i znane mi rozwiązania albo nie działają, albo nie działają offline. Odrzuciłem Firebase i Realm, Stanęło na starym dobrym PouchDB.

Od jakiegoś czasu koduję sobie apkę mobilną w React Native i z pomocą Expo. Przy czym, wiadomo, React Native jest po to, żeby pisać w JavaScripcie aplikacje na urządzenia mobilne, jednak często kończy się tak, że ostatecznie musisz pisać jakiś kod w Javie albo ObjectiveC, bo czegoś nie dało się przeskoczyć. Expo natomiast jest taką nakładką na projekt w React Native, która ma na celu zwolnić cię z obowiązku pisania kodu natywnego i utrzymywania dwóch aplikacji. Zawiera mnóstwo kontrolek, które działają zarówno w Androidzie i iOSie, a także definiuje odpowiednie warstwy abstrakcji na niektóre funkcjonalności systemu, których nie ma bezpośredniego dostępu w React Native. Jednocześnie Expo jest też plaformą, która daje ci podgląd aplikacji, live reload i możliwość bezpośredniego publikowania w AppStore i Google Play (por. ten wątek na Stack Overflow).

No więc koduję sobię tę apkę i nadszedł taki moment, że trzeba było wybrać, w jaki sposób trzymać dane aplikacji.

Przy czym ważnym dla mnie kryterium była też kompatybilność z Expo. Nie chciałbym odłączać projektu od Expo, bo podejrzewam, że jego utrzymanie byłoby trudniejsze. Jeśli to możliwe, zawsze lepiej mieć jedną wersję aplikacji niż kilka.

Firebase

Najpierw próbowałem z Firebase. Pomyślałem sobie, że jest rozwiązanie bardzo popularne, ponoć ma ładne API, autoryzację, funkcje w chmurze, żyć nie umierać. Zainstalowałem paczkę przez NPM i zabrałem się za autoryzację. Trzeba było od tego zacząć, ponieważ developerzy Firebase słusznie przewidzieli, że najcześciej dostęp do danych w aplikacji ma użytkownik zalogowany. Dlatego domyślnie wszystko dzieje się w kontekście zalogowanego użytkownika.

Pierwszy zgrzyt – okropnie ciężko było mi to zrobić dla React Native. Mówiąc szczerze, nie doszedłem do końca. Siedziałem kilka godzin, po czym pomyślałem sobie, że coś tu nie gra. Przecież React Native wydaje się być całkiem popularny, Firebase też, oba rozwiązania teoretycznie powinny się pięknie uzupełniać, więc niemożliwe, że ich zestawienie zajmuje tyle czasu. Albo ja robię coś źle, albo jednak to ze sobą tak fajnie nie współgra. (Firebase ma oczywiście rekomendowany sposób autoryzacji, który jednak nie działa dla React Native; ten issue wisi już ponad rok).

Wtedy porzuciłem na chwilę eksperymenty i postanowiłem więcej poczytać informacji o Firebase i przede wszystkim poszukać informacji o tym, dlaczego jest złe. Zawsze, kiedy chcę nabrać dystansu do jakiegoś rozwiązania, szukam informacji punktujących jego wady 😉.

Nie zawiodłem się, w sieci można znaleźć kilka dobrych artykułów podchodzących krytycznie do googlowego produktu (np. ten, ten i ten, z czego szczególnie ten ostatni jest fajny). Znaczna część wad tak w zasadzie niekoniecznie dotyczy samego Firebase, ale ogólnie rozwiązań tego typu, np. wady rozproszonych baz NoSQL, których nie da się uniknąć ani w Realm, ani w PouchDB. Interesujące są za to bardziej miękkie rzeczy:

  1. Koszty Firebase czasami mogą być nieprzewidywalne, kiedy wpadnie się w odpowiedni przedział wykorzystania danych.
  2. Nie ma łatwego sposobu na wyeksportowanie twoich danych (trzymanych na serwerach, których nie posiadasz). Trzeba pisać maile do Firebase.
  3. Firebase jest zamknięte. Kiedyś było coś podobnego, co się nazywało Parse, zostało kupione przez Facebooka i zamknięte po kilku latach.

Prawda jednak jest też taka, że Firebase pewnie nie jest takie złe, tyle że po prostu nie do końca nadaje się do mojego zastosowania, bo stoi za nim trochę inna filozofia. Robię aplikację mobilną, która z założenia jest offline, a dopiero potem można będzie ewentualnie włączyć synchronizację danych. Firebase tymczasem ma bazę danych online, która może sobie radzić z chwilowym brakiem połączenia. Kiedy nie ma połączenia z siecią, Firebase pozwala na odczyt danych z cache (ostatnie 10 MB danych). Oprócz tego, może trzymać kolejkę zadań zapisu, żeby wysłać je na serwer, kiedy połączenie zostanie przywrócenie. Nie jest to baza, która może sprawnie działać offline.

Częściowo piszą o tym w dokumentacji, a częściowo takie informacje można znaleźć na forach, na przykład tutaj, albo tutaj. Cytując wypowiedź z ostatniego linka:

If you want an offline database, I’d recommend using an offline database. Firebase’s realtime database is not that: it is primarily an online database, that continues to work during short to medium term connectivity loss.

Realm

Pamiętam, że jakiś czas temu na Hacker News pojawił się link do informacji, że Realm wspiera już React Native, i że ten link zebrał sporo plusików. Cała Realm Mobile Platform to tak naprawdę dwa produkty: Realm Mobile Database, czyli baza danych, która może działać offline na urządzeniach mobilnych i Realm Object Server, który pod wieloma względami bardzo przypomina Firebase. Umożliwia synchronizację danych, można się zautoryzować, można nawet definiować cloud functions, podobnie jak w produkcie Googla.

Realm architecture (Źródło obrazka: http://realm.io)

Realm ma jednak kilka znaczących zalet, które pod pewnymi względami dają istotną przewagę nad Firebase (fajna dyskusja):

  1. Jest otwarte. Firebase to zamknięta usługa, która chodzi sobie gdzieś w googlowej chmurze, natomiast platformę Realm możesz sobie postawić sam na swoim serwerze.
  2. Baza danych w Firebase to tak naprawdę jeden wielki obiekt, po którym trzeba się poruszać, żeby wyciągnąć odpowiednie dane. Z kolei baza danych w Realm to baza zorientowana obiektowo – jest narzucona struktura dla obiektów, można lepiej odpytywać, nie trzeba pisac mapowania obiektów.
  3. Baza danych w Realm jest offline first, dzięki czemu wydaje się dużo lepiej niż Firebase pasować do rozwijanej przeze mnie aplikacji mobilnej.

Realm ma jednak również wady:

  1. Wysyła zanonimizowane statystyki korzystania z platformy (przynajmniej tak piszą w licencji dla wersji darmowej).
  2. Niejasny jest dla mnie cennik platformy. Wydaje się, że we wszystkich wersjach (darmowej i płatnych) trzeba mieć własny serwer z postawionym Realm Object Server. W wersji darmowej można mieć tylko 3 cloud functions. Jeśli chcesz mieć więcej, musisz mieć wersję płatną za minimum 1.5 tys. dolarów miesięcznie! (Realm jest jednak produktem ciągle nowym i to się pewnie będzie zmieniać).
  3. Nie działa z Expo, o czym możesz poczytać tutaj i tutaj w dość lakonicznych wypowiedziach pracowników Expo.

We recently added SQLite, and you can use PouchDB (https://pouchdb.com/) on top of it. We may add Realm in the future if there is enough demand!

PouchDB

W idealnym świecie, gdyby działała integracja z Expo, gdybym nie obawiał się wysokich kosztów, i gdyby nie było jeszcze gromadzenia informacji o użyciu mojej instancji Object Server, postawiłbym na Realm. Zdefiniowane schema zapisywanych obiektów, przyjaźnie wyglądające API, możliwość autoryzacji i synchronizacji, wszystko to brzmi bardzo fajnie.

Tyle że jest jeszcze jedno fajne, znane mi rozwiązanie, choć ciągle nie rozumiem, dlaczego wydaje mi się tak mało popularne. Bo nie ma firmy, która by na nim zarabia i która ładowałaby w marketing? Bo sam nie użyłem tego nigdy na produkcji i nie wiem, jakie są pułapki?

PouchDB to tak w zasadzie tylko baza danych, jednak ma mnóstwo zalet, które świetnie dostosowują ją do wymagań mojej offline-fist aplikacji mobilnej w React Native.

  1. Jest rozwiązaniem otwartym, dojrzałym i darmowym.
  2. Działa offline i można ją opcjonalnie synchronizować z dowolną bazą wspierającą protokół CouchDB.
  3. Działa z React Native i Expo.
  4. Jest rozwiązaniem baaaardzo elastycznym z licznymi pluginami.

Żeby jednak wyjaśnić, czym tak naprawdę jest PouchDB, nie sposób nie odnieść się do CouchDB i głównej zalety CouchDB, którą jest jej protokół synchronizacji. Wszystkie bazy danych i inne rozwiązania, implementujące ten protokół, mogą się ze sobą synchronizować, i to na zasadzie master-master, bez żadnych uprzywilejowanych node’ów. PouchDB z założenia wspiera ten protokół, dzięki temu może się synchronizować tak samo, jak CouchDB.

Druga sprawa, to to, że PouchDB powstało po to, aby działać w przeglądarce internetowej. To wymusiło specyficzną konstrukcję biblioteki – PouchDB to tak w zasadzie API, czy też fasada na różne mechanizmy zapisywania danych w przeglądarce. Wspiera różnego rodzaju bazy w przeglądarkach: IndexedDB, WebSQL, czy LevelDB, a także bezpośredni zapis do zdalnej bazy z protokołem CouchDB.

PouchDB attempts to provide a consistent API that “just works” across every browser and JavaScript environment, and in most cases, you can just use the defaults.

PouchDB architecture (Źródło cytatu i obrazka: https://pouchdb.com)

Ponieważ samo PouchDB jest takie elastyczne, możliwe było sworzenie wersji, która działała na React Native (pouchdb-react-native). Ma to wszystko sens, ponieważ skoro piszemy aplikacje mobilne w JavaScripcie, to dlaczego by nie korzystać z bibliotek, które działają w przeglądarkach? Samo PouchDB nie działa bezpośrednio w React Native, ale podpimpowane wspomnianą biblioteką już świetnie sobie radzi.

Wersja ta korzysta z AsyncStorage, które jednak ma ustawiony limit 6MB. Można jednak w stosunkowo prosty sposób skorzystać z adaptera na SQLite, które niedawno zostało dorzucone do Expo.

Simple is not easy, trochę czasu mi zajęło, żeby dojść do tego prostego rozwiązania, dlatego od razu podaję przykład, bo nie znalazłem takiego w sieci (będziesz potrzebować bibliotek: pouchdb-react-native oraz pouchdb-adapter-react-native-sqlite i aplikacji na Expo):

import PouchDB from 'pouchdb-react-native';
import {SQLite}  from 'expo';
import SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite';

const SQLiteAdapter = SQLiteAdapterFactory(SQLite);
PouchDB.plugin(SQLiteAdapter);

const db = new PouchDB('my.db', {adapter: 'react-native-sqlite'});

Ciekawostka: kiedy sam to implementowałem kilka dni temu, ta druga biblioteka nie działała. Zaimportowałem więc kod źródłowy (nie ma tego dużo), trochę się pomęczyłem, naprawiłem, zaczęło działać. Chciałem się podzielić i zrobić issue, ale okazało się, że dokładnie godzinę wcześniej problem został naprawiony. Szansa na commitment przeszła mi koło nosa 😉.


No dobra, ale jakie są wady tego całego podejścia z PouchDB?

Po pierwsze, zauważ, jak bardzo różna jest ta sekcja od sekcji o Firebase i o Realm. Tam pisałem bardziej ogólnie i miękko, tutaj znajdziesz dużo technicznych konkretów. Może to wynikać na przykład z tego, że lepiej orientuję się w PouchDB niż w Firebase i Realm, albo że tutaj dalej zaszedłem w implementacji i natrafiłem na różne problemy, które w przypadku tamtych narzędzi pozostały jeszcze przede mną. Może jednak też być tak, że nad takimi niskopoziomowymi zagadnieniami w ogóle nie musiałbym się zastanawiać, gdybym skorzystał z Firebase i Realm, bo to są po prostu rozwiązanie bardziej kompleksowe i wygodniejsze w użyciu.

Po drugie, zarówno Firebase, jak i Realm, dają dodatkowo platformę z autoryzają, synchronizacją i jakimś podstawowym poziomem bezpieczeństwa. W przypadku konfiguracji PouchDB/CouchDB nie mam plaformy, tylko kilka klocków, które będę musiał sam poskładać w całość. Istnieją jakieś rozwiązania, które mogą zapewnić część funkcjonalnoći, np. Cloudant, albo nawet projekt open source z ambicjami stania się następcą Firebase – Hoodie (zob. też, jak sami porównują się z Firebase), jednak to ciągle nie jest to; to tak jakby porównywać Libre Office z Ms Office 😉.

Wreszcie trzecia kwestia – synchronizacja. Jest ona oczywiście świetnie rozwiązana i sprawdzona, problem za to polega na tym, że synchronizacja jest na poziomie bazy. W związku z tym stosuje się nieco nietypowe podejścia, np. osobna baza dla każdego użytkownika, albo roli. Nie ma z tym problemów wydajnościowych, bo bazy CouchDB są bardzo lekkie, jednak mogą być problemy koncepcyjne, kiedy okaże się, że jakieś dane mają być współdzielone pomiędzy użytkownikowi, albo rolami. Jest coś takiego jak selective replication, tyle że ono służy raczej do innych rzeczy niż zapewnienie security.

W ogóle bezpieczeństwo i dostęp do danych w stacku PouchDB/CouchDB to jest całkiem ciekawe zagadnienie i wymaga trochę przestawienia swojego sposobu myślenia. Ale to już materiał na odrębny wpis, na razie odsyłam tylko tutaj.

Podsumowanie

Zamiast jednoznacznego posta z tezą korzystajcie z PouchDB, bo jest najlepsze, wyszło jak zawsze w IT: to zależy. Dla moich potrzeb, dla aplikacji, którą tworzę, rozpoczęcie przygody z PouchDB wydaje się najlepszym rozwiązaniem. Szukałem czegoś offline dla Expo z możlwością dalszego rozwoju, a pomijając kwestie miękkie, finansowe i prawne, ani Firebase, ani Realm nie były w stanie mi tego zaoferować.