Na
Internet , e até no
Habré , existem vários artigos sobre como trabalhar com o Realm. Esse banco de dados é bastante conveniente e requer um esforço mínimo para escrever código, se você puder usá-lo. Este artigo irá descrever o método de trabalho que eu vim.
Os problemas
Otimização de código
Obviamente, toda vez que escrever código de inicialização para um objeto Realm e chamar as mesmas funções para ler e gravar objetos é inconveniente. Você pode envolvê-lo em abstração.
Objeto de acesso a dados de exemplo:
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)
Você pode adicionar muitos métodos úteis ao DAO, por exemplo: para excluir, ler todos os objetos do mesmo tipo, classificação e similares.Todos eles funcionarão com qualquer um dos objetos do Domínio.
Acessado por thread incorreto
Realm é um banco de dados seguro para threads. O principal inconveniente resultante disso é a incapacidade de transferir um objeto do tipo Realm.Object de um thread para outro.
Código:
DispatchQueue.global(qos: .background).async { let objects = yourObjectDAO.read(by: key) DispatchQueue.main.sync { print(objects) } }
Isso dará um erro:
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
Obviamente, você pode trabalhar com o objeto o tempo todo em um encadeamento, mas, na realidade, isso cria certas dificuldades que são melhores para contornar.
Para a solução, é "conveniente" converter Realm.Object em estruturas que serão transferidas silenciosamente entre diferentes segmentos.
Objeto da região:
final class BirdObj: Object { @objc dynamic var id: String = "" @objc dynamic var name: String = "" override static func primaryKey() -> String? { return "id" } }
Estrutura:
struct Bird { var id: String var name: String }
Para converter objetos em estruturas, usaremos implementações de protocolo
Tradutor:
protocol Translator { func toObject(with any: Any) -> Object func toAny(with object: Object) -> Any }
Para Bird, será assim:
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) } }
Agora resta alterar um pouco o DAO para que ele aceite e retorne estruturas, não objetos do Domínio.
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 } } }
O problema parece estar resolvido. Agora o DAO retornará uma estrutura Bird que pode ser movida livremente entre os threads.
let birdDAO = DAO<BirdObj>(translator: BirdTranslator()) DispatchQueue.global(qos: .background).async { let bird = birdDAO.read(by: key) DispatchQueue.main.sync { print(bird) } }
Uma quantidade enorme do mesmo tipo de código.
Tendo resolvido o problema de transferir objetos entre threads, encontramos um novo. Mesmo no nosso caso mais simples, com uma classe de dois campos, precisamos escrever 18 linhas de código adicionais. Imagine se não houver 2 campos, por exemplo 10, e alguns deles não são tipos primitivos, mas entidades que também precisam ser transformadas. Tudo isso cria um monte de linhas do mesmo tipo de código. Uma mudança trivial na estrutura de dados no banco de dados obriga a subir em três lugares.
O código para cada entidade é sempre, em essência, o mesmo. A diferença depende apenas dos campos das estruturas.
Você pode escrever a geração automática, que analisará nossas estruturas emitindo Realm.Object e Translator para cada uma.
A fonte pode ajudar com isso. Já havia um artigo sobre
habra sobre
Mocking com sua ajuda.
Para dominar essa ferramenta em um nível suficiente, eu tinha descrições suficientes de
tags e filtros de
modelos Stencils (com base nos quais o Sourcery foi feito) e
documentação do próprio Sourcery .
Em nosso exemplo específico, a geração de Realm.Object pode ser assim:
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 - Passamos por todas as estruturas.
# 2 - Para cada um, criamos nossa própria classe herdada Object.
# 3 - Para cada campo com o nome do tipo == String, crie uma variável com o mesmo nome e tipo. Aqui você pode adicionar código para primitivas, como Int, Data e outras mais complexas. Eu acho que a essência é clara.
O código para gerar o Translator é semelhante.
{% 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 %}
É melhor instalar o Sourcery através do gerenciador de dependências, indicando a versão para que o que você escreve funcione para todos da mesma maneira e não quebre.
Após a instalação, resta escrever uma linha de código bash para executá-lo no projeto BuildPhase. Ele deve ser gerado antes que os arquivos do seu projeto comecem a compilar.

Conclusão
O exemplo que dei foi bastante simplificado. É claro que em projetos grandes, arquivos como .stencil serão muito maiores. No meu projeto, eles ocupam pouco menos de 200 linhas, gerando 4000 e adicionando, entre outras coisas, a possibilidade de polimorfismo no Reino.
Em geral, não encontrei atrasos devido à conversão de alguns objetos em outros.
Ficarei feliz em receber comentários e críticas.
Referências
Domínio rápidoGithub de origemDocumentação de fontesEstêncil tags e filtros internos do modeloZombando rapidamente com SourceryCriando um aplicativo ToDo usando Realm e Swift