Sur
Internet , et même sur
Habré , il y a un tas d'articles sur la façon de travailler avec Realm. Cette base de données est assez pratique et nécessite un minimum d'effort pour écrire du code, si vous pouvez l'utiliser. Cet article décrira la méthode de travail à laquelle je suis arrivé.
Les problèmes
Optimisation du code
Évidemment, chaque fois que vous écrivez du code d'initialisation pour un objet Realm et appelez les mêmes fonctions pour lire et écrire des objets, cela ne vous convient pas. Vous pouvez l'envelopper dans l'abstraction.
Exemple d'objet d'accès aux données:
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) } }
Utilisation:
let yourObjectDAO = DAO<YourObject>() let object = YourObject(key) yourObjectDAO.persist(with: object) let allPersisted = yourObjectDAO.read(by: key)
Vous pouvez ajouter de nombreuses méthodes utiles au DAO, par exemple: pour supprimer, lire tous les objets du même type, trier, etc. Ils fonctionneront tous avec n'importe lequel des objets du domaine.
Accessible à partir d'un thread incorrect
Realm est une base de données sécurisée pour les threads. Le principal inconvénient qui en résulte est l'impossibilité de transférer un objet de type Realm.Object d'un thread à un autre.
Code:
DispatchQueue.global(qos: .background).async { let objects = yourObjectDAO.read(by: key) DispatchQueue.main.sync { print(objects) } }
Cela donnera une erreur:
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
Bien sûr, vous pouvez travailler avec l'objet tout le temps dans un seul fil, mais en réalité cela crée certaines difficultés qu'il vaut mieux contourner.
Pour la solution, il est «pratique» de convertir Realm.Object en structures qui seront tranquillement transférées entre différents threads.
Objet de domaine:
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 }
Pour convertir des objets en structures, nous utiliserons des implémentations de protocoles
Traducteur:
protocol Translator { func toObject(with any: Any) -> Object func toAny(with object: Object) -> Any }
Pour Bird, cela ressemblera à ceci:
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) } }
Il reste maintenant à modifier légèrement le DAO pour qu'il accepte et renvoie des structures, pas des objets de domaine.
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 } } }
Le problème semble être résolu. Maintenant, le DAO renverra une structure Bird qui peut être librement déplacée entre les threads.
let birdDAO = DAO<BirdObj>(translator: BirdTranslator()) DispatchQueue.global(qos: .background).async { let bird = birdDAO.read(by: key) DispatchQueue.main.sync { print(bird) } }
Une énorme quantité du même type de code.
Après avoir résolu le problème du transfert d'objets entre les threads, nous en avons rencontré un nouveau. Même dans notre cas le plus simple, avec une classe de deux champs, nous devons écrire 18 lignes de code supplémentaires. Imaginez s'il n'y a pas 2 champs, par exemple 10, et certains d'entre eux ne sont pas des types primitifs, mais des entités qui doivent également être transformées. Tout cela crée un tas de lignes du même type de code. Un changement trivial dans la structure des données dans la base de données vous oblige à grimper en trois endroits.
Le code de chaque entité est toujours, dans son essence, le même. La différence en elle ne dépend que des domaines des structures.
Vous pouvez écrire la génération automatique, qui analysera nos structures en émettant Realm.Object et Translator pour chacune.
La sourcerie peut vous y aider. Il y avait déjà un article sur
habra sur
Mocking avec son aide.
Afin de maîtriser cet outil à un niveau suffisant, j'avais suffisamment de descriptions de
balises de
modèles et de filtres Pochoirs (sur la base desquels Sourcery a été créé) et la
documentation de Sourcery elle-même .
Dans notre exemple spécifique, la génération de Realm.Object pourrait ressembler à ceci:
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 - Nous passons par toutes les structures.
# 2 - Pour chacun, nous créons notre propre objet de classe héritière.
# 3 - Pour chaque champ avec le type type == String, créez une variable avec le même nom et type. Ici, vous pouvez ajouter du code pour des primitives telles que Int, Date et les plus complexes. Je pense que l'essence est claire.
Le code de génération de Translator est similaire.
{% 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 %}
Il est préférable d'installer Sourcery via le gestionnaire de dépendances, en indiquant la version afin que ce que vous écrivez fonctionne de la même manière pour tous et ne se casse pas.
Après l'installation, il nous reste à écrire une ligne de code bash pour l'exécuter dans le projet BuildPhase. Il doit être généré avant que les fichiers de votre projet ne commencent à se compiler.

Conclusion
L'exemple que j'ai donné était assez simplifié. Il est clair que dans les grands projets, les fichiers comme .stencil seront beaucoup plus volumineux. Dans mon projet, ils occupent un peu moins de 200 lignes, tout en générant 4000 et en ajoutant, entre autres, la possibilité de polymorphisme dans Realm.
En général, je n'ai pas rencontré de retard dû à la conversion de certains objets en d'autres.
Je serai heureux de toute rétroaction et critique.
Les références
Realm swiftGithub SourceryDocumentation SourceryBalises et filtres de gabarit intégrés au gabaritSe moquer rapidement avec SourceryCréation d'une application ToDo à l'aide de Realm et Swift