В интернете, да и даже на Хабре, есть куча статей о том, как работать с Realm. Эта база данных достаточно удобная и требует минимальных усилий для написания кода, если ей уметь пользоваться. В этой статье будет описан метод работы, к которому пришел я.
Очевидно, что каждый раз писать код инициализации объекта Realm и вызов одних и тех же функций для чтения и записи объектов- неудобно. Можно обернуть абстракцией.
Пример Data Access Object:
Использование:
В DAO можно добавить много полезных методов например: для удаления, чтения всех объектов одного типа, сортировки и т.п.. Все они будут работать с любым из объектов Realm-а.
Realm — потокобезопасная база данных. Основное неудобство, которое из этого возникает — невозможность передать объект типа Realm.Object из одного потока в другой.
Код:
Выдаст ошибку:
Конечно, можно все время работать с объектом в одном потоке, но в реальности это создает определенные сложности, которые лучше обойти.
Для решения “удобно” конвертировать Realm.Object в структуры, которые будут спокойно передаваться между разными потоками.
Объек Realm:
Структура:
Для конвертирования объектов в структуры будем использовать реализации протокола
Translator:
Для Bird она будет выглядеть так:
Теперь остается немного поменять DAO для того, чтобы он принимал и возвращал структуры, а не объекты Realm.
Проблема вроде решена. Теперь DAO будет возвращать структуру Bird, которую можно будет свободно перемещать между потоками.
Решив проблему с передачей объектов между потоками, мы напоролись на новую. Даже в нашем простейшем случае, с классом из двух полей, нам нужно дополнительно написать 18 строк кода. А представьте, если полей не 2 а, к примеру 10, а некоторые из них не примитивные типы, а сущности, которые тоже нужно преобразовать. Все это порождает кучу строк однотипного кода. Тривиальное изменение структуры данных в базе, вынуждает вас лезть в три места.
Код на каждую сущность всегда, по своей сути, один и тот же. Различие в нем зависит только от полей структур.
Можно написать автогенерацию, которая будет парсить наши структуры выдавая Realm.Object и Translator для каждой. В этом может помочь Sourcery. На хабре уже была статья про Mocking с его помощью.
Для того, чтобы на достаточном уровне освоить этот инструмент, мне хватило описания template tags and filters Stencils (на основе которого сделан Sourcery) и докуметации самого Sourcery.
В нашем конкретном примере генерация Realm.Object может выглядеть так:
#1 — Проходим по всем структурам.
#2 — Для каждой создаем свой класс- наследник Object.
#3 — Для каждого поля, у которого название типа == String, создаем переменную с таким же названием и типом. Здесь можно добавить код, как для примитивов типа Int, Date, так и более сложных. Думаю суть ясна.
Аналогично выглядит и код для генерации Translator
Лучше всего устанавливать Sourcery через менеджер зависимостей, с указанием версии, чтобы то, что вы напишите, работало у всех одинаково и не ломалось.
После установки нам остается написать одну строку bash кода для его запуска в BuildPhase проекта. Генерировать он должен перед тем, как начнут компилироваться файлы вашего проекта.
Приведенный мной пример был изрядно упрощен. Понятно, что в больших проектах файлы типа .stencil будут гораздо больше. В моем проекте они занимают чуть меньше 200 строк, при этом генерируя 4000 и добавляя, ко всему прочему, возможность полиморфизма в Realm.
В целом с задержками из-за конвертирования одних объектов в другие я не сталкивался.
Буду рад любым отзывам и критике.
Realm Swift
Sourcery GitHub
Sourcery Documentation
Stencil built-in template tags and filters
Mocking в swift при помощи Sourcery
Создание приложения ToDo с помощью Realm и Swift
Проблемы
Оптимизация кода
Очевидно, что каждый раз писать код инициализации объекта Realm и вызов одних и тех же функций для чтения и записи объектов- неудобно. Можно обернуть абстракцией.
Пример Data Access Object:
struct DAO<O: Object> {
func persist(with object: O) {
guard let realm = try? Realm() else { return }
try? realm.write { realm.add(object, update: .all) }
}
func read(by key: String) -> O? {
guard let realm = try? Realm() else { return [] }
return realm.object(ofType: O.self, forPrimaryKey: key)
}
}
Использование:
let yourObjectDAO = DAO<YourObject>()
let object = YourObject(key)
yourObjectDAO.persist(with: object)
let allPersisted = yourObjectDAO.read(by: key)
В DAO можно добавить много полезных методов например: для удаления, чтения всех объектов одного типа, сортировки и т.п.. Все они будут работать с любым из объектов Realm-а.
Accessed from incorrect thread
Realm — потокобезопасная база данных. Основное неудобство, которое из этого возникает — невозможность передать объект типа Realm.Object из одного потока в другой.
Код:
DispatchQueue.global(qos: .background).async {
let objects = yourObjectDAO.read(by: key)
DispatchQueue.main.sync {
print(objects)
}
}
Выдаст ошибку:
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
Конечно, можно все время работать с объектом в одном потоке, но в реальности это создает определенные сложности, которые лучше обойти.
Для решения “удобно” конвертировать Realm.Object в структуры, которые будут спокойно передаваться между разными потоками.
Объек Realm:
final class BirdObj: Object {
@objc dynamic var id: String = ""
@objc dynamic var name: String = ""
override static func primaryKey() -> String? { return "id" }
}
Структура:
struct Bird {
var id: String
var name: String
}
Для конвертирования объектов в структуры будем использовать реализации протокола
Translator:
protocol Translator {
func toObject(with any: Any) -> Object
func toAny(with object: Object) -> Any
}
Для Bird она будет выглядеть так:
final class BirdTranslator: Translator {
func toObject(with any: Any) -> Object {
let any = any as! Bird
let object = BirdObj()
object.id = any.id
object.name = any.name
return object
}
func toAny(with object: Object) -> Any {
let object = object as! BirdObj
return Bird(id: object.id,
name: object.name)
}
}
Теперь остается немного поменять DAO для того, чтобы он принимал и возвращал структуры, а не объекты Realm.
struct DAO<O: Object> {
private let translator: Translator
init(translator: Translator) {
self.translator = translator
}
func persist(with any: Any) {
guard let realm = try? Realm() else { return }
let object = translator.toObject(with: any)
try? realm.write { realm.add(object, update: .all) }
}
func read(by key: String) -> Any? {
guard let realm = try? Realm() else { return nil }
if let object = realm.object(ofType: O.self, forPrimaryKey: key) {
return translator.toAny(with: object)
} else {
return nil
}
}
}
Проблема вроде решена. Теперь DAO будет возвращать структуру Bird, которую можно будет свободно перемещать между потоками.
let birdDAO = DAO<BirdObj>(translator: BirdTranslator())
DispatchQueue.global(qos: .background).async {
let bird = birdDAO.read(by: key)
DispatchQueue.main.sync {
print(bird)
}
}
Огромное количество однотипного кода.
Решив проблему с передачей объектов между потоками, мы напоролись на новую. Даже в нашем простейшем случае, с классом из двух полей, нам нужно дополнительно написать 18 строк кода. А представьте, если полей не 2 а, к примеру 10, а некоторые из них не примитивные типы, а сущности, которые тоже нужно преобразовать. Все это порождает кучу строк однотипного кода. Тривиальное изменение структуры данных в базе, вынуждает вас лезть в три места.
Код на каждую сущность всегда, по своей сути, один и тот же. Различие в нем зависит только от полей структур.
Можно написать автогенерацию, которая будет парсить наши структуры выдавая Realm.Object и Translator для каждой. В этом может помочь Sourcery. На хабре уже была статья про Mocking с его помощью.
Для того, чтобы на достаточном уровне освоить этот инструмент, мне хватило описания template tags and filters Stencils (на основе которого сделан Sourcery) и докуметации самого Sourcery.
В нашем конкретном примере генерация Realm.Object может выглядеть так:
import Foundation
import RealmSwift
#1
{% for type in types.structs %}
#2
final class {{ type.name }}Obj: Object {
#3
{% for variable in type.storedVariables %}
{% if variable.typeName.name == "String" %}
@objc dynamic var {{variable.name}}: String = ""
{% endif %}
{% endfor %}
override static func primaryKey() -> String? { return "id" }
}
{% endfor %}
#1 — Проходим по всем структурам.
#2 — Для каждой создаем свой класс- наследник Object.
#3 — Для каждого поля, у которого название типа == String, создаем переменную с таким же названием и типом. Здесь можно добавить код, как для примитивов типа Int, Date, так и более сложных. Думаю суть ясна.
Аналогично выглядит и код для генерации Translator
{% for type in types.structs %}
final class {{ type.name }}Translator: Translator {
func toObject(with entity: Any) -> Object {
let entity = entity as! {{ type.name }}
let object = {{ type.name }}Obj()
{% for variable in type.storedVariables %}
object.{{variable.name}} = entity.{{variable.name}}
{% endfor %}
return object
}
func toAny(with object: Object) -> Any {
let object = object as! {{ type.name }}Obj
return Bird(
{% for variable in type.storedVariables %}
{{variable.name}}: object.{{variable.name}}{%if not forloop.last%},{%endif%}
{% endfor %}
)
}
}
{% endfor %}
Лучше всего устанавливать Sourcery через менеджер зависимостей, с указанием версии, чтобы то, что вы напишите, работало у всех одинаково и не ломалось.
После установки нам остается написать одну строку bash кода для его запуска в BuildPhase проекта. Генерировать он должен перед тем, как начнут компилироваться файлы вашего проекта.
Заключение
Приведенный мной пример был изрядно упрощен. Понятно, что в больших проектах файлы типа .stencil будут гораздо больше. В моем проекте они занимают чуть меньше 200 строк, при этом генерируя 4000 и добавляя, ко всему прочему, возможность полиморфизма в Realm.
В целом с задержками из-за конвертирования одних объектов в другие я не сталкивался.
Буду рад любым отзывам и критике.
Ссылки
Realm Swift
Sourcery GitHub
Sourcery Documentation
Stencil built-in template tags and filters
Mocking в swift при помощи Sourcery
Создание приложения ToDo с помощью Realm и Swift