Realm

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 17:49, 17 января 2019.
Realm
Realm.png
Разработчики: realm.io
Выпущена: 2014; 8 years ago (2014)
Постоянный выпуск:

Realm Java 5.8.0 / 6 November 2018 года

Realm Swift 3.11.2 / 15 November 2018 года

Realm Objective‑C 3.11.2 / 15 November 2018 года

Realm JavaScript 2.19.0 / 8 November 2018 года

Realm .NET 3.3.0 / 8 November 2018 года
Состояние разработки: Активное
Написана на: C++
Операционная система: Кросс-платформенное
Тип ПО: NoSQL БД
Лицензия: Бесплатная
Веб-сайт Realm-database

Realm — кросс-платформенная мобильная база данных для iOS (доступная в Swift & Objective-C) и Android. Realm была создана, чтобы стать лучше и быстрее, чем SQLite и Core Data.

Описание

Realm - это нативная NoSQL база данных для Android (Java, Kotlin), iOS (Objective-C, Swift), Xamarin (C#) и JavaScript (React Native, Node.js). Изначально спроектированная как настоящая объектно-ориентированная база данных, Realm отличается от других аналогичных библиотек, тем что рассматривает объекты данных как живые объекты — это значит, что объекты обновляются синхронно. Они мгновенно реагируют на изменения и легко сохраняются. Мобильная база данных Realm распространяется с открытым исходным кодом с 2016 года и бесплатна для разработчиков.

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

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

Системные требования:

  • iOS 8 или выше.
  • OS X 10.9 или выше.
  • Android 5.0 или выше.

Производительность

Благодаря использованию собственного движка, обеспечивающего высокую скорость работы и простоту в применении, мобильная база данных Realm выполняет запросы и синхронизирует объекты значительно быстрее, чем Core Data и SQLite, и осуществляет параллельный доступ к данным без проблем. Это значит, что несколько источников могут получить доступ к одному и тому же объекту без необходимости управлять блокировкой или каких-либо проблем с несогласованностью данных. Данная особенность является серьезным конкурентным преимуществом. Практика показывает, что в большинстве случаев Realm значительно превосходит в скорости не только SQLite, но и другие альтернативные ORM для Android, такие как ORMLite и Greendao.[Источник 1]

Рисунок 1 - Скорость обработки запросов

Рисунок 2 - Скорость записи

Безопасность

Мобильная база данных Realm предлагает службы шифрования для защиты базы на диске с помощью AES 256 + SHA-2 64-разрядного шифрования.Это позволяет все данные, хранящиеся на диске зашифровывать и расшифровывать с помощью алгоритма AES 256 и проверять с помощью технологии HMAC SHA-2.

Наследование

Realm не поддерживает наследование, что является серьёзной проблемой. Любой Realm объект должен или наследоваться от RealmObject или реализовывать интерфейс маркер RealmModel и быть помеченным аннотацией @RealmClass. Наследоваться от существующих Realm объектов нельзя. Рекомендуется использовать композицию вместо наследования.

Ключевые особенности

Live Objects

Realm - это база данных живых объектов - доступные вам данные всегда актуальные и «свежие».[Источник 2] Не существует понятия «выборки» данных с диска, которые необходимо постоянно перезагружать. Нет необходимости дублировать модель данных с диска в память. Это позволяет контроллерам избавиться от постоянных обновлений пользовательского интерфейса, чтения и записи. Чтение и запись могут происходить в разных местах проекта, в разных потоках, из разных процессов или при использовании платформы Realm Platform из любой точки мира. Каждый класс приложения может забыть об устаревших или кэшированных данных, поскольку объекты Realm всегда актуальны.

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

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

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

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

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

С версии Realm Java 0.84(22 октября 2015) появилась возможность использования асинхронных чтения и транзакции.[Источник 3] При асинхронном чтении мы получаем объект сразу, но работа с ним невозможна, пока он не загрузится. Проверять это нужно с помощью функции isLoaded() или вызвать блокирующую функцию load() . Это достаточно неудобно, поэтому тут лучше использовать Rx, преобразуем в observable и получаем загруженный объект в OnNext. Асинхронные операции доступны только в потоках с Looper. Попытка вызова асинхронного запроса, открытого внутри потока без Looper, вызовет исключение IllegalStateException.

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

Transactions

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

Неправильная запись
 val user = database.getUser(1)
button.setOnClickListener { user.name = "Test" }
Правильная запись
 val user = database.getUser(1)
button.setOnClickListener { database.setUserName(user, "Test") }

Транзакции можно производить синхронно и асинхронно.

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

Вместо ручного отслеживания методов .beginTransaction(), .commitTransaction() и .cancelTransaction() обычно используется метод .executeTransaction() , который автоматически обрабатывает begin/commit и отменяет, если произошла ошибка. Однако при реализации конструкции begin/commit мы можем решать, что делать с ошибками, чего нет при использовании execute. Ниже приведен пример использования executeTransaction.

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

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

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

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

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

Open/Close

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

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

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

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

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

Система уведомления об изменениях данных

Рисунок 3 - Система оповещений об изменениях

Хотя Realm всегда выдаёт самые последние свежие данные, пользовательский интерфейс не знает об изменениях, которые произошли. К счастью, база данных Realm также позаботилась об этом. Достаточно просто «подписаться» на изменения данных, и вам будут приходить уведомления о результатах. Realm не только сообщит вам, когда произошли изменения, но и точные индексы объектов, которые были вставлены, обновлены или удалены. Таким образом, вы не только всегда имеете самые свежие данные, но также точно знаете, когда и что изменилось.

Realm поддерживает уведомления об изменении данных, как самого объекта, так и вложенных объектов. Реализовано это с помощью:

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

"Слушать" изменения внутри транзакций запрещено.

Пример использования 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 отлично подходит для MV* архитектур, когда вся реализация прячется за интерфейсом базы данных. Все обращения и выборки происходят в модуле базы данных. При записи объектов открываем Realm, записываем данные и закрываем его, на вход подается только объект для сохранения. Использование Realm (без метода .copyFromRealm()) накладывает серьезные ограничения на использование подхода Clean Architecture. Использовать разные модели данных для разных слоев не получится, пропадает весь смысл live объектов. Также сложности возникают при создании независимых слоев и открытии/закрытии Realm, так как эта операция привязана к жизненному циклу Activity/Fragment. Частичным решением этой проблемы будет создание изолированного слоя получения данных, преобразования объектов и сохранения их в базе данных.

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

In-Memory Realm и Dynamic Realm

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

  • Стандартный Realm — можно получить методами Realm.getDefaultInstance() или с помощью конкретной конфигурации Realm.getInstance(config), конфигураций может быть неограниченное количество, которые можно воспринимать как отдельные базы данных.
  • In-Memory Realm — это Realm, который все записанные данные хранит в памяти, не записывая их на диск. Как только закрывается текущий экземпляр, все данные пропадают. Подходит для кратковременного хранения данных.
  • Dynamic Realm — используется в основном при миграции, позволяет работать с Realm объектами без использования сгенерированных классов RealmObject, доступ осуществляется по именам полей.

Сравнение с SQLite и Core Data

Ниже представлено сравнение Realm с основными конкурентами: SQLite[Источник 4] и Core Data.[Источник 5]

Название Realm SQLite Core Data
Описание Мобильная NoSQL БД часто используется в качестве замены для SQLite Широко используемая в процессе Реляционная СУБД Фрэймворк для работы с данными, котораый позволяет работать с сущностями и их связями, атрибутами (может использоваться как СУБД)
Модель основной базы данных нативная NoSQL БД Реляционная СУБД
Модель вторичной базы данных Хранилище ключ-значение Хранилище ключ-значение
Язык реализации C++ C Objective-C
Поддержка XML нет нет да
Вторичная индексация да да
SQL нет да да
API и другие методы доступа Realm API ADO.NET, JDBC, ODBC macOS, iOS
Поддерживаемые языки программирования Kotlin,C#, Java, JavaScript, Objective-C,Swift C#, C++, Clojure, Java, JavaScript, Objective-C, PHP, Ruby, Python, Scala, Perl, Haskell, Delphi, Lua, Fortran Objective-C, Swift
MapReduce нет да да
Концепция транзакций ACID ACID
Параллелизм да да
Надежность да да да
Концепция in-memory да да да

Источники

  1. Realm — кроссплатформенная мобильная база данных//Jetruby Blog. Дата обновления: 05.11.2018. URL: https://jetruby.com/ru/blog/realm-mobilnaya-baza-dannyh/ (дата обращения: 30.09.2018)
  2. Live Objects and Fine-Grained Notifications//Realm. Дата обновления: 23.11.2018. URL: https://academy.realm.io/posts/live-objects-fine-grained-notifications-realm-update/ (дата обращения: 30.09.2018)
  3. Async Queries & Transactions//Realm Blog. Дата обновления: 08.10.2018. URL: https://realm.io/blog/realm-java-0-84-0/ (дата обращения: 29.09.2018)
  4. System Properties Comparison//DB-engines. Дата обновления: 15.10.2018. URL: https://db-engines.com/en/system/Realm;SQLite (дата обращения: 30.09.2018)
  5. Core Data//Wikipedia. Дата обновления: 03.01.2019. URL: https://en.wikipedia.org/wiki/Core_Data (дата обращения: 30.09.2018)