Sourcery para convertir autom谩ticamente a estructuras de objetos Realm

En Internet , e incluso en Habr茅 , hay muchos art铆culos sobre c贸mo trabajar con Realm. Esta base de datos es bastante conveniente y requiere un esfuerzo m铆nimo para escribir c贸digo, si puede usarla. Este art铆culo describir谩 el m茅todo de trabajo al que llegu茅.

Los problemas


Optimizaci贸n de c贸digo


Obviamente, cada vez que se escribe un c贸digo de inicializaci贸n para un objeto Realm y se llaman las mismas funciones para leer y escribir objetos es un inconveniente. Puedes envolverlo en abstracci贸n.

Ejemplo de objeto de acceso a datos:

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

Uso:

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

Puede agregar muchos m茅todos 煤tiles al DAO, por ejemplo: para eliminar, leer todos los objetos del mismo tipo, ordenar y similares. Todos ellos funcionar谩n con cualquiera de los objetos Realm.

Accedido desde un hilo incorrecto


Realm es una base de datos segura para subprocesos. El principal inconveniente que surge de esto es la incapacidad de transferir un objeto de tipo Realm.Object de un hilo a otro.

C贸digo:

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

Dar谩 un error:

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

Por supuesto, puede trabajar con el objeto todo el tiempo en un hilo, pero en realidad esto crea ciertas dificultades que es mejor sortear.

Para la soluci贸n, es "conveniente" convertir Realm.Object en estructuras que se transferir谩n silenciosamente entre diferentes hilos.

Objeto de reino:

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

Estructura:

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

Para convertir objetos en estructuras, utilizaremos implementaciones de protocolo
Traductor

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

Para Bird, se ver谩 as铆:

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

Ahora queda cambiar ligeramente el DAO para que acepte y devuelva estructuras, no objetos 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 } } } 

El problema parece estar resuelto. Ahora el DAO devolver谩 una estructura Bird que se puede mover libremente entre hilos.

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

Una gran cantidad del mismo tipo de c贸digo.


Habiendo resuelto el problema de transferir objetos entre hilos, nos encontramos con uno nuevo. Incluso en nuestro caso m谩s simple, con una clase de dos campos, necesitamos escribir 18 l铆neas de c贸digo adicionales. Imag铆nese si no hay 2 campos, por ejemplo 10, y algunos de ellos no son tipos primitivos, sino entidades que tambi茅n necesitan ser transformadas. Todo esto crea un mont贸n de l铆neas del mismo tipo de c贸digo. Un cambio trivial en la estructura de datos en la base de datos te obliga a subir a tres lugares.

El c贸digo para cada entidad es siempre, en su esencia, el mismo. La diferencia en esto depende solo de los campos de las estructuras.

Puede escribir generaci贸n autom谩tica, que analizar谩 nuestras estructuras emitiendo Realm.Object y Translator para cada uno. Sourcery puede ayudar con esto. Ya hab铆a un art铆culo sobre habra sobre Burlarse con su ayuda.

Para dominar esta herramienta a un nivel suficiente, ten铆a suficientes descripciones de plantillas de plantillas y filtros Stencils (en base a los cuales se hizo Sourcery) y documentaci贸n de Sourcery .

En nuestro ejemplo espec铆fico, la generaci贸n de Realm.Object podr铆a verse as铆:

 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 - Pasamos por todas las estructuras.
# 2 - Para cada uno, creamos nuestra propia clase de heredero Object.
# 3 - Para cada campo con el tipo de nombre == Cadena, cree una variable con el mismo nombre y tipo. Aqu铆 puede agregar c贸digo para primitivas como Int, Date y otras m谩s complejas. Creo que la esencia es clara.

El c贸digo para generar el traductor es 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 %} 

Es mejor instalar Sourcery a trav茅s del administrador de dependencias, indicando la versi贸n para que lo que escriba funcione para todos de la misma manera y no se rompa.

Despu茅s de la instalaci贸n, nos queda por escribir una l铆nea de c贸digo bash para ejecutarlo en el proyecto BuildPhase. Debe generarse antes de que los archivos de su proyecto comiencen a compilarse.



Conclusi贸n


El ejemplo que di fue bastante simplificado. Est谩 claro que en proyectos grandes, archivos como .stencil ser谩n mucho m谩s grandes. En mi proyecto, ocupan un poco menos de 200 l铆neas, mientras generan 4000 y agregan, entre otras cosas, la posibilidad de polimorfismo en el Reino.
En general, no encontr茅 demoras debido a la conversi贸n de algunos objetos a otros.
Estar茅 encantado de cualquier comentario y cr铆tica.

Referencias


Reino veloz
Sourcery github
Documentaci贸n fuente
Plantilla integrada de etiquetas y filtros de plantilla
Burl谩ndose r谩pidamente con Sourcery
Crear una aplicaci贸n ToDo usando Realm y Swift

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


All Articles