CoreData
Uma Introdução à Persistência em iOS
Neste tutorial aprenderemos um pouco sobre o que é e como funcionam as principais instruções CRUD da framework oficial da Apple a qual permite a manipulação de objetos num banco de dados local: o CoreData.
O que é CoreData?
O CoreData é uma espécie de camada que simplifica o controle e o monitoramento do banco de dados para o programador, ele reduz instruções que em linguagem SQL seriam necessárias dezenas linhas de código em apenas uma ou duas linhas.
O funcionamento geral do CoreData não é algo fácil e envolveria alguns livros para o entendimento do chamado CoreData Stack, assunto o qual não pertence a essa pequena introdução, mas existem alguns conceitos chave que são essenciais para que você use as mais básicas instruções CRUD do CoreData.O primeiro deles é a ideia do que o CoreData faz com seu banco. De forma curta e grossa: ele transforma suas tabelas em classes, suas colunas em atributos dessas classes e suas entidades do banco em instancias dessas classes. Curto e grosso demais? Bom, vamos ver um exemplo:
Como sou um entusiasta e adoro toda a frescura, vou dar um exemplo com meu assunto favorito: vinhos. Criei um pequeno diagrama de entidade aqui com vinhos e uvas, um vinho com um nome, um país de origem e uma vinícola (local de produção do vinho para os leigos) tem uma ou mais uvas as quais têm um nome (aqueles nomes estranhos que ninguém entende e todo mundo tira sarro: Carménère, Cabernet Sauvignon, Pinot Noir, Syrah e etc etc) e um tipo, que seria os dois básicos: tinta (roxa) e branca. Só para sair do mundo técnico.
A partir daquele diagrama, o que o CoreData faz é transformar cada entidade numa classe e as colunas nos atributos dessa classe:
Esqueça por enquanto os imports, a herança ali e o @NSManaged
… fica para depois entender isso. Fato é, perceba o espelhamento do banco na classe, bem legal não é? Agora como ficaria para a entidade do vinho? Por ele ter uma relação 1 para muitos unidirecional com a uva a classe fica um pouco diferente:
Perceba que todos os atributos anteriores parecem iguais, menos o uvas que é do tipo NSSet
, pois é: como um vinho tem várias uvas ele cria um atributo na classe vinho que é um arranjo de uvas, é desta forma que ele lida com a herança (O tipo NSSet
seria como um Set comum só que capaz de ser controlado pelo CoreData, existe também o NSOrderedSet
que seria um arranjo ordenado caso fôssemos necessitar de uma ordem).
Por fim vamos entender como isso funciona com os CRUD’s:
- Para criar (inserir algo no banco) pedimos para o CoreData uma espécie de instância dessa classe, colocamos os valores que queremos e salvamos tudo.
- Para ler pedimos para o CoreData nos trazer os elementos de uma determinada tabela.
- Para alterar na tabela podemos simplesmente pedir os elementos do CoreData e alterar seus valores diretamente, depois salvar no banco.
- Por fim, para deletar pedimos para o CoreData os elementos e mandamos para ele mesmo quais queremos deletar.
Simples, não? Esta ferramenta é realmente genial e até parece mágica.
SetUp do CoreData no projeto
O XCode tem um sistema lindo dentro dele que facilita muito todo o setup do seu banco no projeto. É realmente bem simples.
No clima de vinhos vamos fazer um programa simples para iOS que o usuário registra um vinho e atribuí uvas a ele. Uma table view, com um + no topo direito para adicionar uma uva nova, aparece o formulário ele entra com as informações e salva. Aparece a uva na lista, ele clica em editar para poder excluir ou alterar as informações da uva, se clicar apenas na uva mostra os vinhos da uva, uma outra table view com o + no topo direito para adicionar um novo vinho. Para essa pequena demonstração, vamos até aí.
Não se esqueça de na hora que criar o projeto assinalar o “Use CoreData”, caso contrário não aparecerá todos os recursos necessários. Existe um pequeno passo que faremos agora que trata-se de uma boa prática muito utilizada pelos grandes aplicativos a fim de facilitar o acesso a um método e uma variável, os quais são utilizados de forma recorrente para acessar certos recursos do CoreData, entrarei em mais detalhes depois.
Crie uma classe num arquivo separado chamada DatabaseController
, entre no seu AppDelegate
e copie e cole nessa classe o seguinte trecho de código:
Estes métodos são gerados automaticamente pelo XCode. A variável persistentContainer
trata-se de uma espécie de caixa, nela existem todos os objetos que estão sendo trabalhados pelo CoreData, como se fosse um log das alterações no banco de dados. Já a função saveContext
pega esse monte de alterações e dá um flush, uma descarga nelas, e persiste no banco, salva as alterações da caixa.
Calma! Alguns erros vão estourar sim, relaxe. Na classe DatabaseController
:
- Importe o CoreData
- Troque o acesso do
persistenceContainer
delazy
parastatic
- Troque o acesso do
saveContext
parastatic
. - No
AppDelegate
no métodoapplicationWillTerminate
estará estourando um erro, troque oself.savecontext()
porDatabaseController.saveContext()
.
Este passo é importante para facilitar o acesso a esse método e essa variável (coisa que é feita de forma recorrente ao longo do programa). Se fôssemos utilizar da forma padrão teríamos que fazer o seguinte: da UIApplication
pegar um singleton o qual possuí uma instância do AppDelegate
, fazer um cast para AppDelegate
e desta instância acessar o persistentContainter e o saveContext()
, como é feito abaixo. No nosso método apenas acessamos utilizando o DatabaseController
diretamente: 4 linhas viram apenas 1.
Pronto, terminamos com a boa prática aqui, no fim a classe fica mais ou menos assim para deixar claro:
Agora partiremos para criar o nosso banco, veremos agora como é lindo a criação do nosso banco no XCode. Assinalar aquele check dizendo que queríamos utilizar o CoreData criou um arquivo chamado NomeDoSeuProjeto.xcdatamodeld
. Esse é o diagrama de entidade do seu projeto, vamos ver como funciona esse editor.
Ele é bem simples de usar: para adicionar uma entidade apenas clique no Add Entity no canto inferior esquerdo, a partir daí ele aparece na lista de Entities a esquerda onde você pode alterar seu nome. No meio você tem os atributos da entidade escolhida, para adicionar uma nova basta clicar no “+”. Aparecerá uma nova com um nome default e um tipo indefinido. Para o tipo existem apenas alguns que podem ser usados, os primitivos, escolha bem eles na hora de criar seu banco para não ter que realizar conversões a toda hora ou salvar coisas desnecessárias. Veja como ficou minha entidade Vinho.
Agora o mesmo para uva:
Muitas alterações podem ser feitas em um atributo. Podemos no editor mesmo adicionar validações, expressões regulares, dizer se os atributos são obrigatórios ou não, transientes, se possuem valores padrão, etc etc etc. Maioria dessas alterações são feitas no inspetor do lado direito.
Então criaremos uma relação entre elas. Para fazer isso basta adicionar uma relação nova, como qualquer relação ela tem um destino e o inverse
que seria quando tanto a uva e o vinho tivessem uma a outra. No meu caso isso não é necessário, mas se quisesse bastaria criar uma relação na uva, definir o destino como a entidade vinho e o inverso como a relação Uvas que está na entidade Vinho.
Por último vamos dar uma olhada nas propriedades das relações, faltou definir que uma uva tem vários vinhos e não um vinho só, para isso basta ir no inspetor e selecionar o tipo para to Many: (Perceba que também temos a possibilidade de deixar a relação ordenada, assim conforme adicionamos uvas à entidade, elas são alocadas de forma ordenada).
Feito! Nosso banco está pronto e estruturado, agora tudo que precisamos fazer é transformar esse diagrama em classe. Apesar de muitos considerarem boa prática fazer esse passo a mão, vamos utilizar uma ferramenta do Xcode que transforma o banco em classes automaticamente. Para isso basta ir em Editor>Create NSManagedObject subclass, escolher as entidades que deseja e o lugar de destino no seu projeto.
O Xcode irá automaticamente criar dois arquivos para cada entidade, um que é a classe em branco e outra que é uma extensão dessa classe com os atributos e alguns métodos padrão, parecido com isso:
Essas são as classes que vimos antes, essas são as representações das entidades, tudo que fizermos com elas estaremos fazendo com o banco. As classes são extensões do NSManagedObject
o que diz que elas são entidades do banco, toda entidade do banco também é um NSManagedObject
. Todo atributo da entidade é uma variável na classe com a notação @NSManaged
dizendo que aquela variável é um atributo da entidade, se o atributo da entidade fosse transiente, não teria essa notação. Para a entidade do vinho, foram criados algumas coisas a mais para administrar a relação que a entidade vinho tem com a entidade da uva, mas veremos isso com mais detalhes depois.
Create
Por uma questão de clareza vou pular alguns passos de storyboard aqui. Em geral no storyboard principal criei uma TableView como a RootViewController de uma NavigationController, deixei ela dinâmica e com as células no estilo subtitle e coloquei um identifier para elas, adicionei um “+” no topo direito e criei uma classe TableViewController
chamada UvaTableViewController
. Algo assim:
Não vamos mexer ainda nos delegates do UITableViewController
, vou ensinar a adicionar uma nova entidade no banco. Crie o action do botão + na UvaTableViewController
e dê o nome de createGrape
. Não se esqueça de importar o banco de dados. Para criarmos uma uva precisamos saber seu nome e tipo, para isso vamos perguntar ao usuário por dois alertas, um depois do outro, por isso não estranhe a classe seguinte:
Bom feito isso só precisamos na linha 31 pegar o grapeName
, o grapeType
e salvar no banco, como diz o comentário “Aqui criaremos a entidade”. Criar algo no banco de dados é o mesmo que instanciar a classe que aquela entidade representa, só que não diretamente. Para tal temos que pedir para uma classe chamada NSEntityDescription
instanciar essa classe que desejamos e colocar essa instância no contexto que queremos, depois fazer um cast para o tipo Uva. Pronto, temos a instancia, basta definir os valores e salvar:
Fácil, não? Uma coisa que deixei para avisar agora é que toda vez que fazemos uma alteração no contexto temos que salvá-lo: DatabaseController.saveContext()
isso faz o commit do que está no contexto e salva efetivamente no banco. A nova entidade está feita e salva.
Mas pera! Não consigo vê-la na lista. Bom vamos aprender a fazer um fetch, uma busca, um…
Read
Perceba que até agora o mais chato não tem sido o banco de dados efetivamente e sim as coisas a parte disso tipo coletar as informações da uva, criar a tabela, criar a navigation controller, etc etc. Não será diferente para essa seção.
A nossa tabela deve ter uma lista com as Uvas a serem apresentadas, então vamos criar uma lista de Uvas, na classe mesmo. Vamos chamá-la de grapes, para inovar um pouco, depois instanciar ela como uma lista vazia.
Bem simples, nada demais. Antes de entrar na parte que enchemos essa lista com as uvas do banco, vamos entender um método lá na classe Uva. Vá para a extensão da classe Uva e procure o método fetchRequest
. Pois é, dá bastante medo esse método, mas relaxe que ele não tem nada demais.
@nonobjc public class func fetchRequest() -> NSFetchRequest<Uva> {
return NSFetchRequest<Uva>(entityName: “Uva”)
}
NSFetchRequest
é uma classe que segura uma serie de informações de busca do banco, existem maneiras de especificar uma busca bem detalhadamente no banco, buscando pela uva de nome X e tipo Y. Essa classe pede um tipo, que basicamente é o tipo do conjunto de coisas que ela vai retornar e pede um nome que é a entidade que se relaciona. Quando simplesmente instanciamos um NSFetchRequest
diretamente (como acontece nesse método pavoroso), basicamente estamos elaborando um pedido para o banco com todos os elementos daquela entidade. O interessante é que toda entidade tem um método pronto desse, ele é gerado automaticamente quando geramos as classes, por isso não precisamos instanciar, basta usar Entidade.fetchRequest()
.
Agora, precisamos mostrar esse pedido para o contexto para que nos sejaM dadaS as uvas, para isso existe um método dentro do contexto chamado fetch que pede como parâmetro: um NSFetchRequest
. Simples assim. O que complica é que esse método pode retornar um erro e precisa de um cast para uma lista de uvas: por isso precisamos envolve-lo com um do catch e adicionar um if let para realizar um cast seguro.
Para a busca vamos colocar no método viewWillAppear
para que toda vez que o usuário for entrar nessa tela, o app busque do banco as uvas, assim nunca faltará uva. No final a busca fica desta forma:
Simples, não? Agora vamos testar, funciona! Apareceu a uva que criei antes! Agora vamos criar outra uva e ver o que acontece… é… ops… Ah quase me esqueci precisa atualizar nossa lista quando criamos uma nova uva. Para isso vamos criar um método chamado reloadGrapeData()
, nele vamos colocar todo esse bloco do “do e catch” e logo abaixo um reloadData
da tableview, vamos substituir todo esse bloco pela chamada da função e lá em cima depois que criar a uva vamos chamar essa função. Pronto! Tudo certo para prosseguirmos.
Update
Para o update vamos fazer o seguinte: se ele clicar numa uva aparecerão os vinhos, certo? Nesta tela teremos uma toolbar com editar do lado esquerdo para ele poder alterar as informações de uma uva.
Para isso criaremos uma TableView no storyboard que será a lista dos Vinhos, para acessá-la o usuário clicará numa célula da TableView de uvas por isso conectaremos uma Segue da célula para essa TableView. Nessa tableview adicionaremos uma bar button item no toolbar e colocaremos ele como do tipo edit.
Precisamos também criar a classe VinhoTableViewController
, essa lista de vinhos é relativa a uma uva específica então terá que ter uma uva atrelada, por isso criaremos uma variável do tipo uva, como propriedade dessa classe. Por fim ligaremos o action do editar nesta view controller a um método que daremos o nome de editGrape
.
Precisamos dizer para a VinhoTableViewController
de qual uva estamos falando, precisamos passar a instância da uva daquela lista na UvaTableViewController
para a VinhoTableViewController
. Faremos isso no prepareForSegue
da UvaTableViewController
:
Método bem simples, não? Agora faremos a edição na VinhoTableViewController
, veja como ficou o método:
A estrutura está bem parecida com o método de adicionar uma uva feito no item de Create, o que mudou basicamente foi na hora de adicionar a uva (linha 40). Perceba que não precisamos trazer nada do banco, o CoreData cuida de rastrear as instâncias e o lugar da memória que estão localizadas, por isso quando salva o contexto verifica automaticamente as modificações e as commita para o banco.
Por isso o update não tem segredo: basta mudar a instância trazida do banco, não importa como: se ela foi buscada ou se ela foi criada, o CoreData cuida de rastrear e commitar essa mudanças para o banco de dados.
Delete
A última letra do CRUD é tão fácil quanto todas as outras. Para deletar vamos nos aproveitar da edição padrão do iOS de uma TableView, aquele editar lá em cima que quando o usuário clica a tabela entra em modo de edição e ele pode remover os itens que deseja. Para habilitar isso é simples, coloque no viewDidLoad
da UvaTableViewController
:
Agora na subscrição do método do UITableViewDelegate
que é chamado quando o usuário deleta alguma célula precisamos remover a uva do banco, para isso basta buscar da lista e pedir para o contexto remover essa instância do banco, depois salvar o contexto, simples assim. Preciso ressaltar que do jeito que está relacionado no banco o CoreData remove por cascata os vinhos também, mas não deleta a uva caso delatássemos o vinho, há formas de se configurar isso caso seja necessário.
CRUD dos vinhos
Bom, descrevi todo o CRUD para as uvas, para os vinhos trata-se de algo bem similar, por isso não descreverei linha a linha de código, apenas irei ressaltar alguns pontos sobre cada uma das operações, já que agora estaremos tratando de uma relação no banco e não de uma entidade separada.
A criação merece um pouco de atenção, porque não basta pedirmos uma instância para o NSEntityDescription
e modificarmos ela depois salvarmos, temos também que relacionar essa nossa instância ao vinho, caso contrário essa união nunca será efetuada.
O que muda basicamente é que temos que utilizar a função addToVinhos
para criar a relação é a forma mais segura de gerar essa união entre a uva e o vinho e não utilizando diretamente o método append no NSSet
de vinhos na uva.
A leitura é mais simples: ela não requer que busquemos do banco os vinhos ou que busquemos a uva para depois pegarmos os vinhos: o CoreData atualiza automaticamente a lista, então apenas temos que limpar a lista de vinhos e adicionar todos os elementos de uma nova buscando da propriedade uva da classe VinhoTableViewController
.
Por último é necessário ressaltar quando removemos um elemento do banco. A boa prática diz para primeiro utilizarmos o método removeFromVinhos
da entidade/classe uva e depois removermos o vinho do contexto para aí salvar no banco, agora se não removermos a união e simplesmente tirarmos do contexto também funciona.
Bom, isso é tudo para esse tutorial. Expliquei passo a passo de como funciona os CRUD’s básicos do CoreData, a partir daí você pode procurar mais sobre mais possibilidades dessa poderosa ferramenta de banco de dados, o CoreData pode salvar até imagens se desejado e não se trata de algo difícil. Vou deixar o meu contato para dúvidas e o projeto em questão no GitHub para estudo.