Sourcery zur automatischen Konvertierung in Realm-Objektstrukturen

Im Internet und sogar in Habré gibt es eine Reihe von Artikeln über die Arbeit mit Realm. Diese Datenbank ist sehr praktisch und erfordert nur minimalen Aufwand, um Code zu schreiben, wenn Sie sie verwenden können. Dieser Artikel beschreibt die Arbeitsmethode, zu der ich gekommen bin.

Die Probleme


Codeoptimierung


Offensichtlich ist es unpraktisch, jedes Mal Initialisierungscode für ein Realm-Objekt zu schreiben und dieselben Funktionen zum Lesen und Schreiben von Objekten aufzurufen. Sie können es in Abstraktion einwickeln.

Beispiel für ein Datenzugriffsobjekt:

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

Verwendung:

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

Sie können dem DAO viele nützliche Methoden hinzufügen, zum Beispiel: Löschen, Lesen aller Objekte des gleichen Typs, Sortierens und dergleichen. Alle funktionieren mit allen Realm-Objekten.

Zugriff von falschem Thread


Realm ist eine thread-sichere Datenbank. Die größte Unannehmlichkeit, die sich daraus ergibt, ist die Unfähigkeit, ein Objekt vom Typ Realm.Object von einem Thread auf einen anderen zu übertragen.

Code:

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

Es wird ein Fehler ausgegeben:

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

Natürlich können Sie die ganze Zeit in einem Thread mit dem Objekt arbeiten, aber in Wirklichkeit führt dies zu bestimmten Schwierigkeiten, die besser zu umgehen sind.

Für die Lösung ist es „praktisch“, Realm.Object in Strukturen zu konvertieren, die leise zwischen verschiedenen Threads übertragen werden.

Reichsobjekt:

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

Struktur:

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

Um Objekte in Strukturen zu konvertieren, verwenden wir Protokollimplementierungen
Übersetzer:

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

Für Bird wird es so aussehen:

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

Jetzt muss das DAO noch geringfügig geändert werden, damit es Strukturen akzeptiert und zurückgibt, nicht Realm-Objekte.

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

Das Problem scheint gelöst zu sein. Jetzt gibt das DAO eine Bird-Struktur zurück, die sich frei zwischen den Threads bewegen lässt.

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

Eine riesige Menge der gleichen Art von Code.


Nachdem wir das Problem der Übertragung von Objekten zwischen Threads gelöst hatten, stießen wir auf einen neuen. Selbst in unserem einfachsten Fall mit einer Klasse von zwei Feldern müssen wir zusätzliche 18 Codezeilen schreiben. Stellen Sie sich vor, es gibt keine zwei Felder, zum Beispiel 10, und einige davon sind keine primitiven Typen, sondern Entitäten, die ebenfalls transformiert werden müssen. All dies erzeugt eine Reihe von Zeilen des gleichen Codetyps. Eine geringfügige Änderung der Datenstruktur in der Datenbank zwingt Sie dazu, an drei Stellen zu klettern.

Der Code für jede Entität ist im Wesentlichen immer derselbe. Der Unterschied hängt nur von den Bereichen der Strukturen ab.

Sie können eine automatische Generierung schreiben, die unsere Strukturen analysiert, indem Sie jeweils Realm.Object und Translator ausgeben. Sourcery kann dabei helfen. Es gab bereits einen Artikel über Habra über Mocking mit seiner Hilfe.

Um dieses Tool auf einem ausreichenden Niveau zu beherrschen, benötigte ich eine Beschreibung der Vorlagen-Tags und Filterschablonen (auf deren Grundlage Sourcery hergestellt wurde) und eine Dokumentation von Sourcery selbst .

In unserem speziellen Beispiel könnte die Generierung von Realm.Object folgendermaßen aussehen:

 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 - Wir gehen alle Strukturen durch.
# 2 - Für jedes erstellen wir unsere eigene Vererbungsklasse Object.
# 3 - Erstellen Sie für jedes Feld mit dem Typnamen == String eine Variable mit demselben Namen und Typ. Hier können Sie Code für Grundelemente wie Int, Date und komplexere hinzufügen. Ich denke, die Essenz ist klar.

Der Code zum Generieren des Übersetzers sieht ähnlich aus.

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

Es ist am besten, Sourcery über den Abhängigkeitsmanager zu installieren und dabei die Version anzugeben, damit das, was Sie schreiben, für alle gleich funktioniert und nicht kaputt geht.

Nach der Installation bleibt es uns überlassen, eine Zeile Bash-Code zu schreiben, um ihn im BuildPhase-Projekt auszuführen. Es muss generiert werden, bevor die Dateien Ihres Projekts kompiliert werden können.



Fazit


Das Beispiel, das ich gegeben habe, war ziemlich vereinfacht. Es ist klar, dass in großen Projekten Dateien wie .stencil viel größer sind. In meinem Projekt belegen sie etwas weniger als 200 Zeilen, erzeugen 4000 und fügen unter anderem die Möglichkeit des Polymorphismus in Realm hinzu.
Im Allgemeinen gab es keine Verzögerungen aufgrund der Konvertierung einiger Objekte in andere.
Ich freue mich über Feedback und Kritik.

Referenzen


Reich schnell
Sourcery Github
Sourcery Dokumentation
Integrierte Vorlagen-Tags und Filter für Schablonen
Mit Sourcery schnell verspotten
Erstellen einer ToDo-Anwendung mit Realm und Swift

Source: https://habr.com/ru/post/de460867/


All Articles