Introducción a Core Data — Parte 1
Uno de los temas que un desarrollador debe manejar (sino es que dominar), creo yo, es la persistencia de datos. Para nosotros los desarrolladores de apps en iOS, Apple nos ha proporcionado un framework llamado Core Data que nos facilita administrar los objetos de nuestra capa de datos.
Sin embargo, Core Data puede ser un reto en cuanto aprendizaje y puede prestarse a confusiones dado que toman lugar diferentes actores y conceptos.
En esta primera parte de dos artículos, daremos una introducción a Core Data, señalaremos los principales actores y explicaremos los distintos conceptos mediante un ejercicio sencillo.
¿Qué es Core Data y por qué utilizarlo?
Como mencionamos, Core Data es un framework de Apple disponible en Mac OS y iOS que nos proporciona diferentes mecanismos o soluciones a tareas comunes “asociados con el ciclo de vida de objetos” o “grafo de objetos”, incluyendo la persistencia de datos.
Es decir, si bien es cierto, que la persistencia de datos puede lograrse de diferentes maneras como escritura/lectura de archivos, serialización/deserealización…con Core Data: a) visualizamos nuestros datos o modelo en forma de grafo de objetos (entidades y relaciones) b) nos facilita la interacción con la tecnología subyacente como lo es SQL, a decir verdad, nos aisla de ello logrando que enfoquemos nuestros esfuerzos en desarrollar la lógica de nuestra aplicación.
Algunas beneficios que nos ofrece Core Data es:
- El agrupar, filtrar u organizar nuestros datos
- Evitar escribir SQL y lograr queries complejos mediante el uso de objetos Fetch Requests y Predicates.
- Reducir el impacto a nivel memoria (memory footprint) mediante el uso de Faulting. Por ejemplo, si nuestra base de datos cuenta con entidades (ej. “Usuarios”) que se encuentran relacionadas con otras entidades (ej. “Cuentas”, “Autos”), gracias al faulting si realizamos un query a nuestra entidad “no jalamos” / “obtenemos” las otras entidades relacionadas a menos que lo solicitemos. Es decir, no tiene caso obtener todos los coches y todas las cuentas bancarias si lo que buscamos es sólo leer el nombre de un usuario.
Componentes de Core Data
Dentro del framework de Core Data existen diferentes componentes encargados de tareas que van desde manejar las conexiones a nuestra base de datos hasta devolver los objetos en ella. Para hablar de ellos es necesario comenzar con el Core Data Stack.
Core Data Stack
Según la documentación, el Core Data Stack es una colección de objetos, un mediador entre nuestra aplicación y el almacenamiento de datos. Éste se compone de:
- Persistent store coordinator: Es un wrapper de nuestra base de datos, administra las conexiones (lecturas y escritura), lo podemos visualizar como un apuntador a nuestra base de datos..
- NSManagedObjectModel : Describe el schema de nuestra base de datos, es decir, las tablas y relaciones.
- NSManagedObjectContext : Nos permite crear, solicitar o actualizar objetos de nuestra base de datos.
- Persistent container : Encapsula todos los componentes anteriores
La mayor parte del tiempo estaremos interactuando con el NSManagedObjectModel para modelar nuestras entidades, relaciones y propiedades y con el NSManagedObjectContext, que en conjunto con los Fetch Request y Sort Descriptors es utilizado para obtener y ordenar nuestras entidades.
¡Manos a la obra!
No hay mejor forma de entender Core Data que con un ejercicio; el plan es hacer una app que tenga una base de datos que cree y borre registros. Estos registros son Usuarios, cada usuario puede tener una o varias cuentas bancarias.
- Empezamos por abrir Xcode y crear una Single View Application, indicamos cualquier nombre de producto (en este caso colocamos CoreDataDealer), lenguaje Swift, Team y Organization Name que querramos indicar. (Todas las opciones como Use Core Data, Include Unit Tests e Include UI Tests los dejamos vacíos)
2. Abrimos el Storyboard y colocamos al centro de nuestro escena una etiqueta (con número de líneas 0, centrado horizontalmente y verticalmente con respecto a la super view) y 2 botones:
- El primero tendrá de texto “Crear Registros” y estará debajo de la etiqueta. Centrado horizontalmente, 20 puntos de espacio con respecto a la etiqueta.
- El segundo tendrá de texto “Borrar Registros” y estará igualmente centrado horizontalmente y 20 puntos con respecto al botón superior.
3. El tercer paso es crear IBOutlet e IBActions hacia nuestro View Controller, tal como se muestra a continuación:
- La etiqueta tendrá un IBOutlet y será llamada summaryLabel.
- El botón de Crear Registros tendrá un IBAction llamado createRecords
- El botón de Borrar Registros tendrá un IBAction llamado deleteRecords
import UIKitclass ViewController: UIViewController {
var counter = 0 @IBOutlet weak var summaryLabel: UILabel! {
didSet {
summaryLabel.text = “Intento: \(counter)\r\nRegistros en la base: \(0)\r\nÚltimo registro: nil”
}
} @IBAction func createRecords(_ sender: UIButton) {
} @IBAction func deleteRecords(_ sender: UIButton) {
}}
Dependiendo el botón que se presione, ya sea Crear Registros o Borrar Registros, insertaremos o borraremos registros en la base de datos y luego actualizaremos el texto de la etiqueta. Usamos una propiedad llamada counter que contará el número de cambios realizados.
Creando nuestro modelo
Es hora de integrar Core Data a nuestro proyecto, lo primero que vamos hacer es crear un modelo de datos que nos permita diseñar nuestras entidades, sus propiedades y relaciones (si estamos familiarizados con SQL, las entidades vendrían siendo las tablas y las propiedades serían los atributos de las tablas). Para ello:
4. En la barra superior de Menú presionamos New > File, en el buscador ingresamos la palabra “data” para filtrar los resultados. Notaremos que aparecerá Data Model el cual seleccionamos, luego presionamos Siguiente.
Este modelo le indicamos de nombre Banking y lo guardamos dentro de nuestro directorio CoreDataDealer.
5. Damos click en el archivo Banking.xcdatamodeld.
Creando entidades, propiedades y validaciones
Llegó el momento de diseñar nuestro esquema o nuestra base de datos bancaria de tal manera que exista una tabla de usuarios y una tabla de cuentas bancarias, cada una tendrá su serie de propiedades. Tendrán una relación de 1 a muchos…es decir, un usuario puede tener 1 o muchas cuentas bancarias, pero una determinada cuenta solo puede pertenecer a un usuario.
6. En la parte inferior, damos click en la opción + Add Entity, notaremos que se agrega una nueva entidad a la cual podemos dar doble click para poder nombrarla User.
7. Luego, en la sección Attributes damos click en el botón + para crear sus propiedades. En este caso serán 3:
- age: De tipo Integer 16
- lastName: De tipo String
- name: De tipo String
- email: De tipo String
8. Si damos doble click en cualquier propiedad, y abrimos la barra de la derecha. Se desplegarán 3 opciones, la última de la derecha es llamada Data Model Inspector. Esta opción nos permite incluir validaciones, valores por defecto, reglas que deberán cumplir nuestras propiedades, campos que son obligatorios u opcionales …entre otros.
Por ejemplo para la edad, establecemos que sea una propiedad obligatoria (al dar uncheck a la opción Optional) y que cumpla con las siguientes validaciones :
- Tenga un valor mínimo de 18 años (dado que se necesita ser mayor de edad para abrir una cuenta bancaria)
- Una valor máximo de 100
- Una valor por defecto de 18
Para el nombre, apellidos y correo, indicamos las siguientes validaciones:
- Uncheck al Optional (para que sean datos obligatorios)
- Longitud mínima de 2 caracteres
- Longitud máxima de 255 caracteres
9. Ahora toca el turno de modelar la tabla de cuentas bancarias, nuevamente en la parte inferior damos click en la opción +Add Entity y luego doble click para renombrarla a Accounts.
A esta entidad le agregamos los siguientes atributos:
- amount de tipo Double, obligatorio
- openingDate de tipo Date, obligatorio
- name de tipo String, obligatorio
10. Agregamos una validación a la propiedad amount estableciendo un valor mínimo de 1000.
Relacionando entidades
Llegó el momento de relacionar las 2 entidades (lo que señalamos que un usuario cuenta con múltiples cuentas pero una cuenta pertenece a un usuario).
11. En la parte inferior > Editor Style damos click en la segunda opción (botón con pequeños cuadrados)
12. Notaremos que se muestra de forma gráfica nuestras entidades y propiedades. Damos click en la entidad User, mantenemos presionado la tecla Ctrl y seleccionamos la entidad Account (esta mecanica es muy parecida a como relacionamos botones, etiquetas e IBActions de nuestros Nibs o Storyboards a nuestro ViewController).
13. Notaremos que ya se creó una relación entre ambas entidades con nombre newRelationship. En la entidad User, damos doble click a esta nueva relación justo como se muestra en la imagen:
14. Cambiamos el nombre de esta relación a accounts, y dado que un usuario puede tener muchas cuentas, en la opción Type colocamos “To Many” .
La regla de borrado (Delete Rule) establece como se van a comportar las entidades relacionadas al borrarse una de ellas. Seleccionamos la opción Cascade para que, una vez borrado un usuario se borren sus cuentas tambien.
15. Seleccionamos la entidad Account y de forma similar, cambiamos el nombre de la relación a belongsTo, el Type to One (puesto que una cuenta solo puede pertenecer a un usuario).
En Delete Rule establecemos la opción Nullify puesto que si deseamos borrar una cuenta, no borremos el usuario asociado a ella.
¡Hora de la programación!
Ya creamos nuestro modelo, es momento de hacer la lógica encargada de crear registros para almacenarlos en la base de datos, pero primero tenemos que inicializar nuestro Core Data Stack.
Paso 1. Inicializando Core Data Stack
En versiones anteriores a iOS 10 y Mac OS 10.12, inicializar este Stack era un proceso complejo que involucraba cargar el modelo de una URL, crear un persistent coordinator, un managed context main queue…etc. Sin embargo, a partir del iOS 10 y Mac OS 10.12, contamos con un objeto NSPersistentContainer que simplifica esta creación, provee un contexto por defecto y una serie de funciones útiles.
Para inicializar nuestro Core Data Stack:
- Creamos un nueva clase en Swift presionando New > File > Swift File, la clase la nombraremos como CoreDataManager y la guardamos en la ruta sugerida por Xcode.
- Una vez creada, implementamos la clase de la siguiente manera:
import Foundation//1
import CoreDataclass CoreDataManager { //2
private let container : NSPersistentContainer! //3
init() { container = NSPersistentContainer(name: “Banking”)
setupDatabase() }
private func setupDatabase() { //4
container.loadPersistentStores { (desc, error) in if let error = error {
print(“Error loading store \(desc) — \(error)”)
return
} print(“Database ready!”) }}
Estos son los pasos que estamos haciendo:
- Importamos el framework de Core Data para acceder a su API
- Creamos una propiedad llamada container de tipo NSPersistentContainer que utilizaremos en los diferentes métodos de la clase.
- En el inicializador creamos y asignamos un objeto NSPersistentContainer a nuestra propiedad, el argumento corresponde al nombre de nuestro modelo.
- El método loadPersistentStores se encarga de inicializar y completar el Core Data Stack. Cuenta con un callback que se manda llamar cada que se cree un persistent store (en nuestro caso solo contamos con uno). Ahora bien, en caso de existir algún error inicializando nuestro stack, el callback devuelve un objeto Error el cual debemos de evaluar y manejar (en este caso solo imprimiremos el mensaje de error).
- Para probar que nuestro CoreDataManager funcione adecuadamente, regresamos a nuestro ViewController y creamos una nueva propiedad de la siguiente manera.
class ViewController: UIViewController {
private let manager = CoreDataManager() ...}
- Si compilamos y corremos nuestro programa, en consola debemos de ver el siguiente texto:
Database ready!
Paso 2. Crear y guardar usuarios
Una vez inicializado el stack, ya podemos crear entidades, almacenarlos en nuestro base y obtenerlos (con la ayuda de un objeto fetch request). Para ello, creamos las siguientes funciones dentro de nuestra clase Core Data Manager :
// 1
func createUser(name : String, lastName : String, email : String, initialAmount : Double, completion: @escaping() -> Void) { // 2
let context = container.viewContext
// 3
let user = User(context: context)
user.name = name
user.lastName = lastName // 4
let account = Account(context: context)
account.name = “Cuenta de \(name)”
account.amount = initialAmount
account.openingDate = Date()
account.belongsTo = user // 5
do { try context.save()
print(“Usuario \(name) guardado”)
completion() } catch {
print(“Error guardando usuario — \(error)”) }}
- Creamos una nueva función dentro de la clase para crear un nuevo usuario junto con su cuenta bancaria, este va a recibir el nombre, apellido, correo, monto inicial de su cuenta y un callback que nos indique cuando el usuario haya sido creado.
- En la mayor parte del tiempo, para poder interactuar con la base de datos es necesario contar con un NSManagedObjectContext. Afortunadamente, nuestro contenedor nos proporciona uno mediante la propiedad viewContext, la cual se encuentra asociada al Main Queue (esto dado que existen otros contextos asociados a otros hilo de ejecución y son ideales cuando necesitamos trabajar sobre un número grande de registros).
- Creamos un objeto usuario utilizando como parámetro un contexto. Asociamos sus propiedades con los parámetros recibidos en el método
- Creamos un objeto cuenta, de igual manera con el contexto proporcionado por el contenedor. Asociamos la cuenta con el usuario mediante la propiedad belongsTo.
- Aunque hemos creado los usuarios, todavia no los hemos almacenado en la base de datos, para ello tenemos que utilizar la función save del NSManagedObjectContext. Esta puede lanzar excepciones por lo que es necesario manejarlo con un try / catch.
Ahora bien, para poder obtener los registros, generamos la siguiente función:
func fetchUsers() -> [User] { //1
let fetchRequest : NSFetchRequest<User> = User.fetchRequest() do {
//2
let result = try container.viewContext.fetch(fetchRequest)
return result } catch { print(“El error obteniendo usuario(s) \(error)”) }
//3
return []}
- Obtenemos un objeto NSFetchRequest mediante la función de clase fetchRequest indicando mediante <> el tipo de objeto que esperamos recibir (en este caso User). Mediante sus otras propiedades, el NSFetchRequest nos permite personalizar el número de registros a recuperar, filtrar resultados con base a predicados…etc, pero para mantener simple el ejercicio no usaremos dichas propiedades y nos limitaremos a obtener todos los registros almacenados.
- Invocamos el método fetch del viewContext del contenedor para obtener un arreglo de usuarios, este bloque puede arrojar una excepción por lo que se debe contemplar dentro de un bloque try/catch. Si no existe excepción, nuestra función devolverá el arreglo señalado.
- En caso de existir algúna excepción en el fetch, nuestra función devolverá un arreglo vacío.
Para poder probar nuestros métodos, regresamos a nuestro ViewController e implementamos las siguientes funciones:
@IBAction func createRecords(_ sender: UIButton) {//1
manager.createUser(name: “Juan”, lastName: “Perez”, email: “juanperez@mail.com”, initialAmount: 2000.50) { [weak self] in //2
self?.updateUI() }}func updateUI() { //3
counter = counter + 1
let users = manager.fetchUsers()
summaryLabel.text = “Intento: \(counter) \r\nRegistros en la base: \(users.count) \r\nÚltimo registro: \(users.last?.name)”}
- Dentro de nuestro IBAction createUser, mandamos llamar el método createUser de nuestro CoreDataManager con los parámetros indicados.
- Mediante el callback de la función, el ViewController es notificado cuando un usuario se creó y procedemos a actualizar la interfaz con una nueva función llamada updateUI
- Cada que se actualice la interface, aumentamos nuestro contador a uno y hacemos una petición a la base de datos para obtener los usuarios previamente creados. Luego actualizamos el texto de la etiqueta para que refleje: a) el número de cambios/intentos realizados, b) el número de registros en la base y c) despliegue el último usuario capturado.
Llegó el momento de probar la creación de los usuarios, para eso compilamos y ejecutamos el proyecto.
Al aparecer el simulador, si damos click al botón Crear Registros…
Notaremos que en consola se despliega el siguiente mensaje de error:
Error guardando usuario — Error Domain=NSCocoaErrorDomain Code=1570 “The operation couldn’t be completed. (Cocoa error 1570.)” UserInfo={NSValidationErrorObject=<User: 0x600000bf0690> (entity: User; id: 0x6000028d6a20 <x-coredata:///User/t68E1E915–009F-45D5–9E9F-551E405743962> ; data: {accounts = (“0x6000028c3d60 <x-coredata:///Account/t68E1E915–009F-45D5–9E9F-551E405743963>”);age = 18;email = nil;lastName = Perez;name = Juan;}), NSValidationErrorKey=email, NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1570.)}
Recordemos que cuando estuvimos diseñando las entidades en nuestro modelo, implementamos unas reglas de validación para las propiedades del usuario…entre ellas el correo.
Si revisamos nuestra función createUser notaremos que ni siquiera hemos asignado el correo a la propiedad email del usuario y nosotros lo marcamos como un campo obligatorio :v
Por ello, la implementación final de createUser es la siguiente:
func createUser(name : String, lastName : String, email : String, initialAmount : Double, completion: @escaping() -> Void) {
let context = container.viewContext
let user = User(context: context)
user.name = name
user.lastName = lastName
user.email = email let account = Account(context: context)
account.name = “Cuenta de \(name)”
account.amount = initialAmount
account.openingDate = Date()
account.belongsTo = user
do { try context.save()
print(“Usuario \(name) guardado”)
completion() } catch {
print(“Error guardando usuario — \(error)”) }}
Si compilamos y corremos el proyecto, cada vez que presionemos el botón Crear Registros notaremos que se almacena un registro en la Base de Datos…
…y en consola se despliega el texto indicando un guardado satisfactorio.
Usuario Juan guardado
Conclusión
En esta primera parte dimos un primer vistazo de lo que es Core Data, sus componentes principales y lo que nos permite hacer.
Por ejemplo, con la ayuda de nuestro modelo, diseñamos nuestras entidades/relaciones y establecimos reglas de validación para sus propiedades.
Con la ayuda del NSPersistentContainer pudimos inicializar nuestro stack y obtener el contexto que nos permite crear y guardar registros. Mientras que con el fetch request podemos recuperarlos.
Sin embargo, de nuestro ejercicio queda pendiente el tema de borrado de registros y resolver otras dudas , la más importante es ¿cómo se comporta Core Data cuando tenemos que trabajar con una gran cantidad de registros? Eso lo revisaremos en la segunda parte.
Esten pendientes y si encontraron útil este artículo, no olviden darle aplauso o compartirlo :P