- PVSM.RU - https://www.pvsm.ru -

Реалистичный Realm. 1 год опыта

Реалистичный Realm. 1 год опыта - 1

Realm давно известен в среде мобильных (и не только) разработчиков. К сожалению, в рунете почти нет статей об этой базе данных. Давайте исправим эту ситуацию.

Ровно год назад в build.gradle нашего проекта появилась строчка:

classpath "io.realm:realm-gradle-plugin:0.89.1" 

За этот год код Realm вырос до версии 3.3, обзавелся множеством фич и починил кучу багов, реализовал новый функционал и получил облачный бекенд. Давайте поподробнее поговорим о Realm в реалиях Andoroid разработки и обсудим тонкие моменты, возникающие при его использовании.

О нас

Мы разрабатываем приложение для коммуникации внутри команды, что-то среднее между telegram и slack. Android приложение написано на Kotlin, с самого начала использовался подход offline-first, т.е. когда все данные, отображенные на экране, достаются из кэша. Попробовав несколько разных баз данных, мы остановились на Realm и в течении года активно использовали его. Данная статья выросла из внутреннего документа по использованию Realm. Статья не является переводом документации и не претендует на полноту описания, это скорее сборник рецептов и разбор тонких моментов. Для полного понимания строго рекомендуем прочитать официальную документацию [17]. Мы же расскажем о своем опыте и какие шишки набили за этот год. Весь код для статьи написан на Kotlin, найти его вы можете на Github [18].

Realm как стартап

Если говорить о Realm как о компании [19], то это датский стартап основанный в 2011 году. Ранее проект назывался tight.db. За время существования привлечено 29M$ инвестиций. Зарабатывать компания планирует на основе Realm Mobile Platform [20], сама же база данных бесплатная и опенсорсная. Realm под Android появился в 2014 году и с тех пор постоянно развивается. Некоторые апдейты ломают обратную совместимость, однако фиксы можно сделать достаточно легко и быстро.

Realm как база данных

Realm — это база данных для нескольких платформ. О себе они пишут:

The Realm Mobile Platform is a next-generation data layer for applications. Realm is reactive, concurrent, and lightweight, allowing you to work with live, native objects.

Если кратко, то это нативная no-sql база данных для Android (Java, Kotlin), iOS (Objective-C, Swift), Xamarin (C#) и JavaScript (React Native, Node.js).
Так же есть backend, который позволяет синхронизировать данные из всех источников.

Из ключевых особенностей стоит отметить zero copy [21], MVCC [22]и ACID [23]. Встроенного механизма устаревания и очистки данных нет.

У Realm есть очень хорошая документация [17] и множество примеров на github [24].
Сотрудники Realm периодически мониторят StackOverflow [25], также можно завести issue на github [26].

Hello world

Hello world под Android выглядит следующим образом:

Добавим в build.gradle

build.gradle (Project level) 

classpath "io.realm:realm-gradle-plugin:3.3.0"

build.gradle (App level) 

apply plugin: 'realm-android'

В Application настроим Realm Configuration

Realm.init(this)
val config = RealmConfiguration.Builder()
       .build()
Realm.setDefaultConfiguration(config)

И можно начинать работать с базой данных:

val realm = Realm.getDefaultInstance()

realm.executeTransaction { realm ->
   val dataObject = realm.createObject(DataObject::class.java)
   dataObject.name = "A"
   dataObject.id = 1
}

val dataObject = realm.where(DataObject::class.java).equalTo("id", 1).findFirst()
dataObject.name // => A

realm.executeTransaction { realm ->
   val dataObjectTransaction = realm.where(DataObject::class.java).equalTo("id", 1).findFirst()
   dataObjectTransaction.name = "B"
}
dataObject.name // => B

Сравнение с другими базами данных

На хабре есть статья от 8 апреля 2016 года, где сравниваются 9 ORM под Android [27], в том числе Realm. Realm там в лидерах, вот графики:

Сравнение с другими ORM

image

image

На своем сайте Realm приводит следующую статистику [28]:

Графики с сайта Realm

Реалистичный Realm. 1 год опыта - 4
Реалистичный Realm. 1 год опыта - 5

Можно выделить три главные особенности, которые необходимо учитывать:
Live Objects — Все объекты, полученные из Realm, являются, по сути, прокси к базе данных. За счет этого достигается zero copy (объекты не копируются из базы)
Transactions — Все изменения привязанных объектов данных нужно проводить внутри транзакции
OpenClose — Необходимость открытиязакрытия instance базы данных

Live Objects

Все объекты из Realm можно получить синхронно или асинхронно.

Синхронное чтение

fun getFirstObject(realm: Realm, id: Long): DataObject? {
   return realm.where(DataObject::class.java).equalTo("id", id).findFirst()
}

Вызываем метод Realm и блокируем поток, пока не получим объект или null. Использовать объекты, полученные в других потоках, нельзя, поэтому для использования в главном потоке, нужно блокировать ui или использовать асинхронные запросы. К счастью, Realm предоставляет нам прокси, а не сам объект, поэтому все происходит достаточно быстро. С объектом можно работать сразу после получения.

Асинхронное чтение

Весьма неочевидный кейс. Как вы думаете, что произойдет в этом коде:

val firstObject = realm.where(DataObject::class.java).findFirstAsync()
log(firstObject.id)

Правильный ответ: получим ошибку java.lang.IllegalStateException
При асинхронном чтении мы хоть и получаем объект сразу, но работать с ним не можем, пока он не загрузится. Проверять это нужно с помощью функции isLoaded() или вызвать блокирующую функцию load(). Выглядит достаточно неудобно, поэтому тут лучше использовать rx. Преобразуем в observable и получаем загруженный объект в OnNext. Асинхронные операции доступны только в потоках с Looper.

fun getObjectObservable(realm: Realm, id: Long): Observable<DataObject> {
   return realm.where(DataObject::class.java).findFirstAsync().asObservable()
}

Основные особенности Realm объектов

  • Получение объектов из базы очень быстрое, десериализации как таковой нет, чтение с диска происходит только при обращении к конкретному полю
  • Именно для этого существует требование делать все поля приватными и обращаться через геттеры
  • Метод copyFromRealm() — позволяет получать отвязанные, полностью собранные объекты, прямо как обычная ORM. Правда и все фишки Realm становятся недоступны. На вход принимается глубина десериализации, по умолчанию MAX_INT
  • В дебагере все поля будут null. Для получения какого-либо значения нам нужно обращаться через геттер
  • Все объекты Live, т.е живые. Изменения распространяются моментально в рамках одного потока. Более сложные кейсы смотрите ниже (многопоточность).
  • Фильтрация объектов осуществляется по полям, причем названия полей вы указываете руками в виде строки. Например так: .equalTo(«id», 1). Это усложняет рефакторинг и приводит к ошибкам в наименовании полей для фильтрации. К сожалению Realm не генерирует переменные с названиями полей, поэтому все выборки лучше прятать внутри функции:
    fun findFirstDataObject(id: Long, realm: Realm) : DataObject
           =  realm.where(DataObject::class.java).equalTo("id", id).findFirst() 

    или использовать генератор имен полей от cmelchior [29]

  • Этот пункт изменился прямо во время написания статьи (яркий пример того, как развивается проект)
    Было: Невозможно использовать DiffUtil при изменении объекта, невозможно понять какие поля у объекта изменились. Т.е. если вам пришла нотификация об изменении объекта, невозможно понять что у него изменилось. Связано это с тем, что оба объекта (старый и новый) являются live объектами и ссылаются на одни и те же данные, они всегда будут равны.
    Стало: Можно использовать RealmObjectChangeListener [30] для понимания что изменилось:

    RealmObjectChangeListener

    Person p = realm.where(Person.class).findFirst();
                p.addChangeListener(new RealmObjectChangeListener<Person>() {
                    @Override
                    public void onChange(Person person, ObjectChangeSet changeSet) {
                        if (changeSet.isDeleted()) {
                            hide(); // Object was deleted
                        } else {
                            // Use information about which fields changed to only update part of the UI
                            if (changeSet.isFieldChanged("name")) {
                                updateName(person.getName());
                            }
                        }
                    }
                });
     

  • Любой объект доступен только пока открыт instance realm-a, из которого мы его получили. Проверять можно методами isValid. При обращении к невалидному объекту получим исключение
  • Объекты доступны только в том потоке, в котором созданы. Обращаться из другого потока нельзя, получим исключение

Аналогично списки (RealmResult) объектов (результаты запроса) являются прокси к Realm, это приводит к следующему:

  • Получение списков очень быстрое, по сути мы получаем только count. Все запросы lazy, получить большой список из сложных объектов мы можем очень быстро
  • Списки доступны только для чтения, любые методы изменения приводят к исключению
  • Т.к. мы можем быстро и дешево получать все элементы, можно забыть о проблеме пагинации. Мы всегда отдаем полный список элементов, при скролле обращаемся к объектам, и они быстро получаются из базы. Если нам нужно подгрузить данные, мы запускаем загрузку, получаем данные, сохраняем их в Realm, снова получаем полный список с загруженными элементами и отображаем его
  • До недавнего времени (до версии 3.0) была проблема с перерисовкой всех элементов списка. Если мы используем список для адаптера, то при изменении одного элемента происходит полная перерисовка всего списка. Использовать DiffUtils и сравнивать какие объекты изменились, не получится, т.к. это live объекты. В Realm 3.0 появились OrderedCollectionChangeSet, который сообщает нам DeletionRanges, InsertionRange, ChangeRanges. Стало наконец возможно понять какие объекты и как изменились.
    Пример CollectionChangeListener

    
    private final OrderedRealmCollectionChangeListener<RealmResults<Person>> changeListener = new OrderedRealmCollectionChangeListener() {
        @Override
        public void onChange(RealmResults<Person> collection, OrderedCollectionChangeSet changeSet) {
            // `null`  means the async query returns the first time.
            if (changeSet == null) {
                notifyDataSetChanged();
                return;
            }
            // For deletions, the adapter has to be notified in reverse order.
            OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges();
            for (int i = deletions.length - 1; i >= 0; i--) {
                OrderedCollectionChangeSet.Range range = deletions[i];
                notifyItemRangeRemoved(range.startIndex, range.length);
            }
    
            OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges();
            for (OrderedCollectionChangeSet.Range range : insertions) {
                notifyItemRangeInserted(range.startIndex, range.length);
            }
    
            OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges();
            for (OrderedCollectionChangeSet.Range range : modifications) {
                notifyItemRangeChanged(range.startIndex, range.length);
            }
        }
    };
    

Транзакции

Изменять привязанные к Realm объекты можно только внутри транзакции, при изменении вне транзакции получим ошибку. С одной стороны, не очень удобно, с другой стороны — дисциплинирует и не дает изменять объекты в любой части кода, только в определенном слое (database). Также нужно помнить, что транзакции внутри другой транзакции запрещены.

Как нельзя делать:

val user = database.getUser(1)
button.setOnClickListener { user.name = "Test" }

Как можно:

val user = database.getUser(1)
button.setOnClickListener { database.setUserName(user, "Test") }

Транзакции можно производить синхронно и асинхронно. Давайте подробнее рассмотрим каждый из вариантов:

Синхронные транзакции:

fun syncTransaction() {
   Realm.getDefaultInstance().use {
       it.executeTransaction {
           val dataObject = DataObject()
           it.insertOrUpdate(dataObject)
       }
   }
}

Также можно выполнять транзакции между beginTransaction и commitTransaction, однако рекомендуется использовать именно executeTransaction.

К сожалению, синхронные транзакции не поддерживают onError callback, так что обработка ошибок остается на вашей совести. Есть issue [31]на добавление onError callback c июня 2016 года.

Асинхронные транзакции

Асинхронные транзакции запускаются методом asyncTransaction. На вход отдаем саму transaction и callback onSuccess и onError, на выходе получаем объект RealmAsyncTask, с помощью которого мы можем проверить статус или отменить транзакцию. Асинхронные транзакции запускаются только в тредах с Looper. Пример асинхронной транзакции:

Realm.getDefaultInstance().use {
   it.executeTransactionAsync({
       it.insertOrUpdate(DataObject(0))
   }, {
       log("OnSuccess")
   }, {
       log("onError")
       it.printStackTrace()
   })
}

Пара важных нюансов:

Вы не сможете присвоить через сеттер объект, не привязанный к Realm. Необходимо сначала положить объект в базу, а потом прикрепить привязанную копию. Пример:

val realm = Realm.getDefaultInstance()
val parent = realm.where(Parent::class.java).findFirst()
val children = Children()
// parent.setChildren(children) <-- Error
val childrenRealm = realm.copyToRealmOrUpdate(children)
parent.setChildren(childrenRealm) /// Ok

Много транзакций лучше объединять в одну. В Realm есть внутренняя очередь на транзакции (размером 100) и если вы превысите ее, упадет исключение.

Все асинхронные транзакции работают на одном executor’e

// Thread pool for all async operations (Query & transaction)
static final RealmThreadPoolExecutor asyncTaskExecutor = RealmThreadPoolExecutor.newDefaultExecutor();

Если у вас будет много асинхронных операций за короткое время, получите ошибку RejectedExecutionException. Выходом из данной ситуации будет использование отдельного потока и запуск в нем синхронных транзакций или объединение нескольких транзакций в одну.

Open/close realm

Все объекты из базы данных мы получаем, используя конкретный instance Realm-a, и можем работать с ними пока открыт этот instance. Как только мы вызовем realm.close(), любая попытка чтения объекта обернется для нас исключением. Если мы не будем вовремя закрывать Realm, то это приведет к утечкам памяти, т.к. сборщик мусора не умеет корректно работать с ресурсами, используемыми Realm.

В официальной документации [32] рекомендуется открыватьзакрывать Realm:

  • для Activity: onCreate / onDestroy
  • для Fragment: onCreateView/onDestroyView

Однако если вы хотите вынести логику работы с Realm из ActivityFragments в презентеры, вам придется использовать (прокидывать) методы жизненного цикла.

В случае если вам нужно как-то изменить данные или добавить новые, проще всего получить новый instance, записать данные и затем закрыть его. В Kotlin для этого можно использовать .use()

Realm.getDefaultInstance().use { // it = realm instance}

Для чтение объектов с помощью Rx можно использовать “изолированные” instance и закрывать их в doOnUnsubscribe (или использовать Observable.using)

// Use doOnUnsubscribe 
val realm = Realm.getDefaultInstance()
realm.where(DataObject::class.java).findAllSorted("id").asObservable().doOnUnsubscribe { realm.close() }

// Use Observable.using
Observable.using(Realm.getDefaultInstance(), realm -> realm.where(DataObject::class.java).equalTo("id", id)
       .findFirstAsync()
       .asObservable()
       .filter(realmObject -> realmObject.isLoaded())
       .cast(DataObject::class.java), Realm::close);

Также есть особенность, связанная с закрытием Realm в onDestroyonDestroyView. Иногда после закрытия Realm происходит вызов FragmentManagerImpl.moveToState → ViewGroup.removeView →… → RecyclerViewAdapter.getItemCount() и вызывается метод list.size() от невалидной коллекции. Так что тут нужно проверять isValid() или отвязывать adapter от recyclerView

Если вы используете Kotlin Android Extensions [33], то работать с view (из kotlinx.android.synthetic.*) из Fragment можно только начиная с метода onViewCreated(), лучше настраивать все listeners в этом методе, чтобы не получить NPE.

После разбора трех самых важных особенностей, пробежимся по менее важным:

Notifications, RxJava

Realm поддерживает уведомления об изменении данных, причем как самого объекта, так и вложенных объектов (всех залинкованных объектов). Реализовано это с помощью RealmChangeListener (нам приходит сам объект), RealmObjectChangeListener ( приходит измененный объект и ObjectChangeSet для него, можно понять какие поля изменились) или с помощью RxJava (в onNext получаем объект, в случае асинхронного запроса необходимо проверять isLoaded(), работает только в потоках с Looper).

RxJava2 пока не завезли, issue висит с сентября 2016 года [34], когда реализуют — неизвестно, используйте Interop [35].

Аналогично можно слушать изменения коллекций или всего instance Realm. Слушать изменения внутри транзакций запрещено.

Пример Rx:

fun getObjectObservable(realm: Realm, id: Long): Observable<DataObject?> {
   return realm.where(DataObject::class.java).equalTo("id", id).findFirstAsync()
           .asObservable<DataObject?>().filter({ it?.isLoaded }).filter { it?.isValid }
}

Многопоточность и асинхронность

Realm это MVCC база данных. Википедия говорит про MVCC [22]:

“Управление параллельным доступом с помощью многоверсионности (англ. MVCC — MultiVersion Concurrency Control) — один из механизмов обеспечения параллельного доступа к БД, заключающийся в предоставлении каждому пользователю так называемого «снимка» БД, обладающего тем свойством, что вносимые пользователем изменения в БД невидимы другим пользователям до момента фиксации транзакции. Этот способ управления позволяет добиться того, что пишущие транзакции не блокируют читающих, и читающие транзакции не блокируют пишущих.”

На практике это выглядит следующим образом: мы можем слушать изменения объекта или с помощью RxJava получать измененные объекты в onNext. В случае, если изменения происходят в потоке А, а мы работаем с объектом в потоке B, то поток B узнает об изменениях после закрытия Realm instance в потоке A. Изменения передаются посредством Looper. Если в потоке B Looper-a нет, то изменения не дойдут (можно проверить методом isAutoRefresh()). Выход из данной ситуации — использовать метод waitForChange().

Что касается асинхронных вызовов и транзакций, то их лучше не использовать вовсе. Удобнее переводить действия на отдельный поток и там выполнять синхронные операции. Причин несколько [36]:

  • Нельзя смешивать асинхронные транзакции и синхронные, если смешать, все транзакции станут синхронными
  • Нельзя использовать асинхронные вызовы в потоках без Looper
  • Для длительных транзакций нужно открыть отдельный instance realm, иначе realm может быть закрыт во время транзакции и вы получите исключение
  • Все действия в асинхронной транзакции происходят на отдельном внутреннем executor-е, как следствие вы не можете пользоваться внешними realm объектами, возможно переполнение executor-а, изменения realm object не распространяются между потоками и прочие неудобства

Тестирование

Раньше Realm.java — был final и для тестирования нужен был powerMock или другие подобные инструменты. В данный момент Realm.java перестал быть final и можно спокойно использовать обычный mockito. Примеры тестов в демо проекте [18] или на официальном репозитории

Один Realm хорошо, а три лучше

Работая с Realm мы всегда имеем ввиду стандартный realm, однако существуют еще In-Memory Realm и Dynamic Realm.

Стандартный Realm — можно получить методами Realm.getDefaultInstance() или с помощью конкретной конфигурации Realm.getInstance(config), конфигураций может быть неограниченное количество, это по сути отдельные базы данных.

In-Memory Realm — это Realm, который все записанные данные хранит в памяти, не записывая их на диск. Как только мы закроем этот instance, все данные пропадут. Подходит для кратковременного хранения данных.

Dynamic Realm — используется в основном при миграции, позволяет работать с realm — объектами без использования сгенерированных классов RealmObject, доступ осуществляется по именам полей.

Наследование и полиморфизм

Realm не поддерживает наследование. Любой realm-объект должен или наследоваться от RealmObject или реализовывать интерфейс маркер RealmModel и быть помеченным аннотацией @RealmClass. Наследоваться от существующих Realm объектов нельзя. Рекомендуется использовать композицию вместо наследования. Весьма серьезная проблема, issue висит с января 2015 года [37], но воз и ныне там.

Kotlin

Realm из коробки работает c Kotlin [38].
Не работают data class-ы, нужно использовать обычные open class.
Также стоит отметить Kotlin-Realm-Extensions [39], удобные расширения для работы с RealmObject.

Realm mobile platform

Первое время Realm был представлен только базами данных для разных платформ, сейчас они выкатили сервер для синхронизации между всеми устройствами. Теперь платформа состоит из:

  • Realm Mobile Database – база для хранения данных
  • Realm Object Server – сервер, отвечающий за автоматическую синхронизацию и обработку событий
  • Realm Data Integration API – для подключения и синхронизации данных с существующими БД (Oracle, MongoDB, Hadoop, SAP HANA, Postgres и Redis)

Иллюстрация работы mobile platform

Реалистичный Realm. 1 год опыта - 6

Отладка

Для отладки у нас есть несколько инструментов:

  • RealmLog [40] — выводит лог, есть разные уровни логирования
  • Realm браузер [41] — нужен просмотра базы данных с компьютера. Работает только под Mac. Для просмотра базы на Windows можно использовать Stetho Realm [42]
    Также существуют несколько Android библиотек для удобного просмотра данных на девайсе [43].
  • WriteCopyTo() [44] — позволяет скопировать базу в файл и отправить ее на анализ.
  • NDK Debugging [45] — для анализа ошибок в нативном коде можно использовать Crashlytics NDK Crash Reporting

Архитектура

Realm отлично подходит для MV* архитектур, когда вся реализация прячется за интерфейсом базы данных. Все обращения и выборки происходят в модуле базы данных (repository), наверх отдаются Observable c автоматически закрываемым realm при unsubscribe. Или принимаем на вход instance realm и производим все действия с ним. При записи объектов мы открываем realm, записываем данные и закрываем его, на вход подается только объект для сохранения. Оба примера смотрите на github [18].
Увы, использование Realm (без copyFromRealm) накладывает серьезные ограничения на использование clean architecture [46]. Использовать разные модели данных для разных слоев не получится, пропадает весь смысл live объектов и прокси списков. Также сложности возникнут при создании независимых слоев и открытиизакрытии Realm, тк эта операция привязана к жизненному циклу ActivityFragment. Хорошим вариантом будет изолированный слой получения данных, преобразование объектов и сохранение их в базе данных.

Realm очень удобен при построении offline-first приложений, когда все данные для отображения мы получаем из базы данных.

Полезные ссылки

Для продолжения знакомства и разбора тонких моментов, рекомендуем следующие статьи:

Три статьи статьи от @Zhuinden [47]:
Basics of Realm: A guide to using Realm 1.2.0 [48]
How to use Realm for Android like a champ, and how to tell if you’re doing it wrong [49]
Realm 1.2.0 + Android Data Binding

Две статьи про интеграцию Realm от @Viraj.Tank [50]
Safe-Integration of Realm in Android production code, Part-1 with MVP
Deep integration of Realm in Android production code, Part-2, with MVP [51]

Многопоточность, подробный разбор:
Designing a Database: Realm Threading Deep Dive
Docs — Auto-Refresh [52]
Docs — Threading [53]

Недавняя статья на хабре от FairBear:
Как подружиться с Realm

Заключение

Realm сложнее, чем кажется на первый взгляд. Однако все недостатки с лихвой покрываются его мощностью и удобством. Live объекты, нотфикации и Rx, удобное API и множество других вещей упрощают создание приложений. Из конкурентов можно выделить requery, ObjectBox и GreenDao. Полностью же Realm раскрывает себя при построении offline-first приложений, когда все данные мы получаем из кэша и нам необходимы сложные выборки, а также постоянное обновление данных.
Весь приведенный код вы можете найти на Github [18]

Автор: andrey7mel

Источник [54]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android-development/256721

Ссылки в тексте:

[1] О нас: https://habrahabr.ru/post/328418/#1

[2] Realm как стартап: https://habrahabr.ru/post/328418/#2

[3] Realm как база данных: https://habrahabr.ru/post/328418/#3

[4] Сравнение с другими базами данных: https://habrahabr.ru/post/328418/#5

[5] Live Objects — Асинхронное чтение: https://habrahabr.ru/post/328418/#7

[6] Основные особенности Realm объектов : https://habrahabr.ru/post/328418/#8

[7] Транзакции: https://habrahabr.ru/post/328418/#9

[8] Синхронные транзакции: https://habrahabr.ru/post/328418/#10

[9] Open/close realm: https://habrahabr.ru/post/328418/#12

[10] Notifications, RxJava: https://habrahabr.ru/post/328418/#13

[11] Многопоточность и асинхронность: https://habrahabr.ru/post/328418/#14

[12] Тестирование: https://habrahabr.ru/post/328418/#15

[13] Kotlin: https://habrahabr.ru/post/328418/#18

[14] Отладка: https://habrahabr.ru/post/328418/#20

[15] Архитектура: https://habrahabr.ru/post/328418/#21

[16] Заключение: https://habrahabr.ru/post/328418/#23

[17] официальную документацию: https://realm.io/docs/java/latest/

[18] Github: https://github.com/andrey7mel/realm_example

[19] Realm как о компании: https://www.crunchbase.com/organization/realm-2#/entity

[20] Realm Mobile Platform: https://realm.io/pricing/

[21] zero copy: https://ru.wikipedia.org/wiki/Zero-copy

[22] MVCC : https://ru.wikipedia.org/wiki/MVCC

[23] ACID: https://ru.wikipedia.org/wiki/ACID

[24] примеров на github: https://github.com/realm/realm-java/tree/master/examples

[25] мониторят StackOverflow: https://stackoverflow.com/questions/tagged/realm

[26] issue на github: https://github.com/realm/realm-java/issues

[27] статья от 8 апреля 2016 года, где сравниваются 9 ORM под Android: https://habrahabr.ru/post/281226/

[28] следующую статистику: https://realm.io/news/realm-for-android/

[29] использовать генератор имен полей от cmelchior: https://github.com/cmelchior/realmfieldnameshelper

[30] Можно использовать RealmObjectChangeListener: https://news.realm.io/news/realm-java-3-1/

[31] Есть issue : https://github.com/realm/realm-java/issues/3048

[32] официальной документации: https://realm.io/docs/java/latest/#controlling-the-lifecycle-of-realm-instances

[33] Kotlin Android Extensions: https://kotlinlang.org/docs/tutorials/android-plugin.html

[34] issue висит с сентября 2016 года: https://github.com/realm/realm-java/issues/3497

[35] Interop: https://github.com/akarnokd/RxJava2Interop

[36] Причин несколько: https://medium.com/@ffvanderlaan/realm-auto-updated-objects-what-you-need-to-know-b2d769d12d76

[37] issue висит с января 2015 года: https://github.com/realm/realm-java/issues/761

[38] работает c Kotlin: https://realm.io/docs/java/latest/#kotlin

[39] Kotlin-Realm-Extensions: https://github.com/vicpinm/Kotlin-Realm-Extensions

[40] RealmLog: https://realm.io/docs/java/latest/api/io/realm/log/RealmLog.html

[41] Realm браузер: https://github.com/realm/realm-browser-osx

[42] Stetho Realm: https://github.com/uPhyca/stetho-realm

[43] просмотра данных на девайсе: https://github.com/jonasrottmann/realm-browser

[44] WriteCopyTo(): https://realm.io/docs/java/latest/api/io/realm/Realm.html#writeCopyTo-java.io.File-

[45] NDK Debugging: https://realm.io/docs/java/latest/#ndk-debugging

[46] clean architecture: https://github.com/android10/Android-CleanArchitecture

[47] @Zhuinden: https://medium.com/@Zhuinden/

[48] Basics of Realm: A guide to using Realm 1.2.0: https://medium.com/@Zhuinden/basics-of-realm-a-guide-to-using-realm-1-2-0-634471c0fe8f

[49] How to use Realm for Android like a champ, and how to tell if you’re doing it wrong: https://medium.com/@Zhuinden/how-to-use-realm-for-android-like-a-champ-and-how-to-tell-if-youre-doing-it-wrong-ac4f66b7f149

[50] @Viraj.Tank: https://medium.com/@Viraj.Tank/

[51] Deep integration of Realm in Android production code, Part-2, with MVP: https://medium.com/@Viraj.Tank/deep-integration-of-realm-in-android-production-code-part-2-with-mvp-4cf44ab6289d

[52] Docs — Auto-Refresh: https://realm.io/docs/java/latest/#auto-refresh

[53] Docs — Threading: https://realm.io/docs/java/latest/#threading

[54] Источник: https://habrahabr.ru/post/328418/