Sourcery для автоматического конвертирования в структуры объектов Realm

от автора

В интернете, да и даже на Хабре, есть куча статей о том, как работать с Realm. Эта база данных достаточно удобная и требует минимальных усилий для написания кода, если ей уметь пользоваться. В этой статье будет описан метод работы, к которому пришел я.

Проблемы

Оптимизация кода

Очевидно, что каждый раз писать код инициализации объекта 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *