Проблемы
Оптимизация кода
Очевидно, что каждый раз писать код инициализации объекта 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
ссылка на оригинал статьи https://habr.com/ru/post/460867/
Добавить комментарий