Sourcery自动转换为Realm对象结构

Internet上 ,甚至在Habré上 ,都有关于如何使用Realm的大量文章。 该数据库非常方便,并且如果可以使用它,则只需很少的精力即可编写代码。 本文将介绍我来到的工作方法。

问题所在


代码优化


显然,每次为Realm对象编写初始化代码并调用相同的函数来读写对象都是不方便的。 您可以将其抽象包装。

示例数据访问对象:

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对象一起使用。

从错误的线程访问


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转换为将在不同线程之间静默传输的结构是“方便的”。

领域对象:

 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 } 

要将对象转换为结构,我们将使用协议实现
译者:

 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可以为此提供帮助。 关于habra的文章已经有关于模拟的帮助。

为了充分掌握该工具,我对模板标签和过滤器 模板 (基于制作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-对于每个类型名称==字符串的字段,创建一个具有相同名称和类型的变量。 在这里,您可以为原语(例如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中多态的可能性。
通常,由于某些对象转换为其他对象,我没有遇到延迟。
我将很高兴收到任何反馈和批评。

参考文献


境界迅捷
源码github
原始资料
模具内置模板标签和过滤器
快速了解Sourcery
使用Realm和Swift创建ToDo应用程序

Source: https://habr.com/ru/post/zh-CN460867/


All Articles