Sourcery to automatically convert to Realm object structures

On the Internet , and even on Habré , there are a bunch of articles on how to work with Realm. This database is quite convenient and requires minimal effort to write code, if you can use it. This article will describe the working method that I came to.

Problems


Code optimization


Obviously, each time writing initialization code for a Realm object and calling the same functions to read and write objects is inconvenient. You can wrap it in abstraction.

Example Data Access Object:

struct DAO {
    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)
    }
}

Using:

let yourObjectDAO = DAO()
let object = YourObject(key)
yourObjectDAO.persist(with: object)
let allPersisted = yourObjectDAO.read(by: key)

You can add many useful methods to the DAO, for example: to delete, read all objects of the same type, sort, etc. All of them will work with any of the Realm objects.

Accessed from incorrect thread


Realm is a thread safe database. The main inconvenience that arises from this is the inability to transfer an object of type Realm.Object from one thread to another.

The code:

DispatchQueue.global(qos: .background).async {
    let objects = yourObjectDAO.read(by: key)
    DispatchQueue.main.sync {
        print(objects)
    }
}

It will give an error:

Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'

Of course, you can work with the object all the time in one thread, but in reality this creates certain difficulties that are better to circumvent.

For the solution, it’s “convenient” to convert Realm.Object into structures that will be quietly transferred between different threads.

Realm Object:

final class BirdObj: Object {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    override static func primaryKey() -> String? { return "id" }
}

Structure:

struct Bird {
    var id: String
    var name: String
}

To convert objects into structures, we will use the
translator protocol implementations :

protocol Translator {
    func toObject(with any: Any) -> Object
    func toAny(with object: Object) -> Any
}

For Bird, it will look like this:

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)
    }
}

Now it remains to slightly change the DAO so that it accepts and returns structures, not Realm objects.

struct DAO {
    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
        }
    }
}

The problem seems to be solved. Now the DAO will return a Bird structure that can be freely moved between threads.

let birdDAO = DAO(translator: BirdTranslator())
DispatchQueue.global(qos: .background).async {
    let bird = birdDAO.read(by: key)
    DispatchQueue.main.sync {
        print(bird)
    }
}

A huge amount of the same type of code.


Having solved the problem of transferring objects between threads, we ran into a new one. Even in our simplest case, with a class of two fields, we need to write an additional 18 lines of code. Imagine if there are no 2 fields, for example 10, and some of them are not primitive types, but entities that also need to be transformed. All this creates a bunch of lines of the same type of code. A trivial change in the data structure in the database forces you to climb into three places.

The code for each entity is always, in its essence, the same. The difference in it depends only on the fields of structures.

You can write auto-generation, which will parse our structures by issuing Realm.Object and Translator for each. Sourcery can help with this . There was already an article on habra about Mocking with its help.

In order to master this tool at a sufficient level, I had enough descriptions of template tags and filters Stencils (on the basis of which Sourcery was made) and documentation of Sourcery itself .

In our specific example, the generation of Realm.Object might look like this:

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 - We go through all the structures.
# 2 - For each, we create our own inheritor class Object.
# 3 - For each field with the type name == String, create a variable with the same name and type. Here you can add code for primitives such as Int, Date, and more complex ones. I think the essence is clear.

The code for generating Translator looks similar.

{% 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 %}

It is best to install Sourcery through the dependency manager, indicating the version so that what you write works for everyone the same way and does not break.

After installation, it remains for us to write one line of bash code to run it in the BuildPhase project. It must be generated before the files of your project begin to compile.



Conclusion


The example I gave was pretty simplified. It is clear that in large projects, files like .stencil will be much larger. In my project, they occupy a little less than 200 lines, while generating 4000 and adding, among other things, the possibility of polymorphism in Realm.
In general, I did not encounter delays due to the conversion of some objects to others.
I will be glad to any feedback and criticism.

References


Realm Swift
Sourcery GitHub
Sourcery Documentation
Stencil built-in template tags and filters
Mocking in swift using Sourcery
Create a ToDo application using Realm and Swift

Also popular now: