Dados principais em detalhes

Recentemente, comecei a trabalhar em um grande projeto usando o Core Data. O habitual é que as pessoas nos projetos mudem, a experiência seja perdida e as nuances sejam esquecidas. É impossível aprofundar todos no estudo de uma estrutura específica - todos têm seus próprios problemas de trabalho. Portanto, preparei uma breve apresentação, a partir desses pontos que considero importantes ou insuficientemente abordados nos tutoriais. Compartilho com todos e espero que isso ajude a escrever um código eficaz e não cometer erros. Supõe-se que você já esteja um pouco no assunto.

Vou começar com o banal.

Dados Principais é uma estrutura que gerencia e armazena dados em um aplicativo. Você pode considerar o Core Data como um invólucro em um armazenamento relacional físico que representa dados na forma de objetos, enquanto o Core Data em si não é um banco de dados.

Objetos de dados principais


imagem

Para criar armazenamento, o aplicativo usa as classes NSPersistentStoreCoordinator ou NSPersistentContainer . O NSPersistentStoreCoordinator cria um armazenamento do tipo especificado com base no modelo, você pode especificar o local e opções adicionais. O NSPersistentContainer pode ser usado com o iOS10, fornece a capacidade de criar com uma quantidade mínima de código.

Funciona da seguinte maneira: se um banco de dados existe no caminho especificado, o coordenador verifica sua versão e, se necessário, faz uma migração. Se o banco de dados não existir, ele será criado com base no modelo NSManagedObjectModel. Para que tudo isso funcione corretamente, antes de fazer alterações no modelo, crie uma nova versão no Xcode através do menu Editor -> Adicionar versão do modelo. Se você encontrar o caminho, poderá encontrar e abrir a base no emulador.

Exemplo com NSPersistentStoreCoordinator
var persistentCoordinator: NSPersistentStoreCoordinator = { let modelURL = Bundle.main.url(forResource: "Test", withExtension: "momd") let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!) let persistentCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!) let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let storeURL = URL(fileURLWithPath: documentsPath.appending("/Test.sqlite")) print("storeUrl = \(storeURL)") do { try persistentCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSSQLitePragmasOption: ["journal_mode":"MEMORY"]]) return persistentCoordinator } catch { abort() } } () 

Exemplo de NSPersistentContainer
 var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoreDataTest") container.loadPersistentStores(completionHandler: { (storeDescription, error) in print("storeDescription = \(storeDescription)") if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container } () 


O Core Data usa 4 tipos de armazenamento:

- SQLite
- binário
- na memória
- XML ​​(apenas Mac OS)

Se, por exemplo, por motivos de segurança, você não desejar armazenar dados em um formato de arquivo, mas ao mesmo tempo desejar usar o cache durante a sessão e dados em forma de objetos, o armazenamento do tipo "In-Memory" é bastante adequado. Na verdade, não é proibido ter vários tipos diferentes de armazenamento em um aplicativo.

Quero dizer algumas palavras sobre o objeto NSManagedObjectContext . Em geral, a Apple fornece uma formulação muito vaga para o NSManagedObjectContext - um ambiente para trabalhar com objetos do Core Data. Tudo isso com o desejo de se dissociar das associações com os bancos de dados relacionais e de apresentar o Core Data como uma ferramenta fácil de usar que não requer um entendimento de chaves, transações e outros atributos do Bazdan. Mas na linguagem dos bancos de dados relacionais, o NSManagedObjectContext pode, em certo sentido, ser chamado de gerenciador de transações. Você provavelmente notou que ele tem métodos de salvamento e reversão, embora provavelmente use apenas o primeiro.

Um mal-entendido desse fato simples leva ao uso de um modelo de contexto único, mesmo em situações em que não é suficiente. Por exemplo, você está editando um documento grande e, ao mesmo tempo, teve que baixar alguns diretórios. Em que momento você chama salvar? Se estivéssemos trabalhando com um banco de dados relacional, não haveria dúvida, pois cada operação seria executada em sua própria transação. Os Dados Principais também têm uma maneira muito conveniente de resolver esse problema - esse é um ramo do contexto filho. Infelizmente, por algum motivo, isso raramente é usado. Aqui há um bom artigo sobre esse tópico.

Herança


Por alguma razão que eu não entendo, há um número muito grande de manuais e exemplos em que a herança para Entity / NSManagedObject (tabelas) não é usada de forma alguma. Enquanto isso, é uma ferramenta muito conveniente. Se você não usar herança, poderá atribuir valores a atributos (campos) apenas através do mecanismo KVC, que não verifica os nomes e tipos de atributos, e isso pode facilmente levar a erros de tempo de execução.

A redefinição de classe para NSManagedObject é feita no designer de Dados Principais:

imagem

Herança e geração de código


Depois de especificar o nome da classe para Entity, você pode usar a geração de código e obter uma classe com código pronto:

imagem

imagem

Se você deseja ver o código gerado automaticamente, mas não deseja adicionar arquivos ao projeto, pode usar outra maneira: defina a opção “Codegen” para Entity. Nesse caso, o código deve ser pesquisado em ../ DerivedData / ...

imagem

Use a geração de código para criar classes, erros de digitação em nomes de variáveis ​​podem levar a erros de tempo de execução.

Aqui está um código como este:

 @objc public class Company: NSManagedObject {    @NSManaged public var inn: String?  @NSManaged public var name: String?  @NSManaged public var uid: String?  @NSManaged public var employee: NSSet? } 

Em breve, @NSManaged tem o mesmo significado que dinâmico no objetivo C.
O próprio Core Data cuida do recebimento de dados (possui acessadores internos) para os atributos de suas classes. Se você tiver campos de trânsito, precisará adicionar funções para calculá-los.

As classes herdadas do NSManagedObject (tabelas) não tinham um construtor "regular" antes do IOS10, ao contrário de outras classes. Para criar um objeto do tipo Company, era necessário escrever uma construção bastante desajeitada usando o NSEntityDescription. Agora, há um método mais conveniente de inicialização por meio do contexto (NSManagedObjectContext). O código está abaixo. Observe a vantagem da herança ao atribuir atributos sobre o mecanismo KVC:

 // 1 -    NSEntityDescription,    KVO let company1 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) company1.setValue("077456789111", forKey: "inn") company1.setValue(" ", forKey: "name") // 2 -    NSEntityDescription,     let company2 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) as! Company company2.inn = "077456789222" company2.name = " " // 3 -     (IOS10+),     let company3 = Company(context: moc) company3.inn = "077456789222" company3.name = " " 

Espaço para nome para NSManagedObject


Outra coisa que vale a pena mencionar é o espaço para nome.

imagem

Você não terá dificuldade se estiver trabalhando no ObjectiveC ou no Swift. Normalmente, esse campo é preenchido corretamente por padrão. Mas em projetos mistos, pode ser uma surpresa para você que, para as aulas de Swift e ObjectiveC, você precise definir opções diferentes. No Swift, o "Módulo" deve ser preenchido. Se esse campo não for preenchido, um prefixo com o nome do projeto será adicionado ao nome da classe, o que causará um erro de tempo de execução. No Objetivo C, deixe o “Módulo” vazio, caso contrário o NSManagedObject não será encontrado ao acessá-lo através do nome da classe.

Links entre objetos


Em princípio, o tópico de relacionamentos é bem abordado, mas quero focar em como adicionar entidades filhas ao pai. Portanto, primeiro, um lembrete rápido do mecanismo para criar links. Considere um exemplo tradicional, a empresa é funcionários, a conexão é uma para muitos:

  • Crie uma conexão de cada lado (tabela)
  • Depois disso, o campo Inverso fica disponível, ele deve ser preenchido em cada tabela.

imagem

imagem

A Apple insiste em especificar relacionamentos inversos. Ao mesmo tempo, a inversão não fortalece a coerência, mas ajuda os Dados Principais a rastrear alterações nos dois lados da conexão, é importante para armazenar em cache e atualizar as informações.

Também é importante especificar a regra de exclusão corretamente. Uma regra de exclusão é uma ação que será executada com esse objeto quando o objeto pai for excluído.

  • Cascata - exclusão de todos os objetos filhos, quando o pai é excluído.
  • Negar - proibição de excluir um pai, se houver um filho
  • Nullify - anula a referência do pai
  • Nenhuma ação - nenhuma ação especificada, ele emitirá um aviso na compilação

Neste exemplo, quando uma empresa é excluída, todos os funcionários serão excluídos (em cascata). Quando você exclui um funcionário, o link para ele na empresa é redefinido (pré-tela)

Maneiras de adicionar entidades filhas ao pai


1) O primeiro método é adicionar via NSSet. Por exemplo, adicione 2 funcionários à empresa:

 let set = NSMutableSet();    if let employee1 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee1.firstName = "" employee1.secondName = "" set.add(employee1) } if let emploee2 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee2.firstName = "" employee2.secondName = "" set.add(employee2) }    company.employee = set 

Este método é conveniente para inicializar o objeto ou preencher o banco de dados. Há uma ligeira nuance. Se a empresa já tiver funcionários e você tiver atribuído um novo conjunto, os ex-funcionários redefinirão o link para a empresa, mas eles não serão excluídos. Como alternativa, você pode obter uma lista de funcionários e trabalhar já com este conjunto.

 let set = company.mutableSetValue(forKey: "employee") 

2) Adicionando objetos filhos via ID pai

 if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee.firstName = "" employee.secondName = "" employee.company = company } 

O segundo método é conveniente ao adicionar ou editar um objeto filho no
formulário separado.

3) Adicionando objetos filho através de métodos gerados automaticamente

 extension Company {    @objc(addEmployeeObject:)  @NSManaged public func addEmployee(_ value: Employee)    @objc(removeEmployeeObject:)  @NSManaged public func removeFromEmployee(_ value: Employee)    @objc(addEmployee:)  @NSManaged public func addEmployee(_ values: NSSet)    @objc(removeEmployee:)  @NSManaged public func removeFromEmployee(_ values: NSSet) } 

Por uma questão de exaustividade, é útil conhecer esse método, mas de alguma forma não foi útil para mim, e excluo o código extra para não atrapalhar o projeto.

Consultas de cláusulas filho


No Core Data, você não pode fazer uma consulta arbitrária entre nenhum dado, como podemos fazer no SQL. Mas entre objetos dependentes, é fácil rastrear usando um predicado padrão. Abaixo está um exemplo de consulta que seleciona todas as empresas nas quais há um funcionário com o nome especificado:

 public static func getCompanyWithEmployee(name: String) -> [Company] { let request = NSFetchRequest<NSFetchRequestResult>(entityName: self.className()) request.predicate = NSPredicate(format: "ANY employee.firstName = %@", name) do { if let result = try moc.fetch(request) as? [Company] { return result } } catch { } return [Company]() } 

A chamada do método no código ficará assim:

 //  ,    let companies = Company.getCompanyWithEmployee(name: "") 

Não use campos de trânsito nas consultas; seus valores não são definidos no momento em que a consulta é executada. Nenhum erro ocorrerá, mas o resultado estará incorreto.

Definindo atributos (campos)


Você provavelmente percebeu que os atributos de entidade têm várias opções.
Com opcional, tudo fica claro a partir do nome.

A opção de usar o tipo escalar apareceu em swif. O Objective-C não usa tipos escalares para atributos, pois eles não podem ser nulos. Tentar atribuir um valor escalar a um atributo através do KVC causará um erro de tempo de execução. Isso deixa claro por que os tipos de atributo no Core Data não possuem uma correspondência estrita com os tipos do idioma. Em projetos rápidos e mistos, os atributos do tipo escalar podem ser usados.

Atributos de transporte público são campos calculados que não são armazenados no banco de dados. Eles podem ser usados ​​para criptografia. Esses atributos recebem valores por meio de um acessador substituído ou pela atribuição de primitivas conforme necessário (por exemplo, willSave e awakeFromFetch substituídos).

Acessadores de atributo:


Se você não precisar usar campos calculados, por exemplo, para criptografia ou outra coisa, não precisará pensar no que são os acessórios de atributo. Enquanto isso, as operações de obtenção e atribuição de valores a atributos não são "atômicas". Para entender o que quero dizer, consulte o código abaixo:

 //  let name = company.name //  company.willAccessValue(forKey: "name") let name = company.primitiveValue(forKey: "name") company.didAccessValue(forKey: "name") //  company.name = " " //  company.willChangeValue(forKey: "name") company.setPrimitiveValue(" ", forKey: "name") company.didChangeValue(forKey: "name") 

Use primitivas nos eventos NSManagedObject em vez da atribuição usual para evitar o loop. Um exemplo:

 override func willSave() {   let nameP = encrypt(field: primitiveValue(forKey: "name"), password: password)   setPrimitiveValue(nameP, forKey: "nameC")   super.willSave() }  override func awakeFromFetch() {   let nameP = decrypt(field: primitiveValue(forKey: "nameC"), password: password)   setPrimitiveValue(nameP, forKey: "name")   super.awakeFromFetch() } 

Se de repente, em algum momento, você precisar parafusar a função awakeFromFetch em um projeto, ficará surpreso que ela funcione de maneira muito estranha, mas, na verdade, ela não é chamada quando você executa a solicitação. Isso ocorre porque o Core Data possui um mecanismo de armazenamento em cache muito inteligente e, se a seleção já estiver na memória (por exemplo, porque você acabou de preencher esta tabela), o método não será chamado. No entanto, meus experimentos mostraram que, em termos de valores calculados, você pode confiar com segurança no uso do awakeFromFetch, conforme a documentação da Apple. Se, para teste e desenvolvimento, você precisar forçar o awakeFromFetch, adicione managedObjectContext.refreshAllObjects () antes da solicitação.

Só isso.

Obrigado a todos que leram até o fim.

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


All Articles