Core Data es otra de las palabrejas con las que tendrás que lidiar si haces una app para iOS más o menos compleja.
Para mi aprender un nuevo lenguaje, es como aprender un nuevo idioma. Al principio, pienso en mi idioma nativo e intento traducirlo al nuevo idioma. Con práctica, tiempo y trabajo, este proceso de “traducción” acaba desapareciendo poco a poco. Mi lenguaje nativo es PHP así que he tratado de buscar similitudes entre Swift y PHP…
Conocí Core Data cuando empecé con Objective C. Poco ha cambiado con Swift y lo he agradecido porque la primera vez que me metí con Core Data, me costó adaptarme a ello.
Core Data no es MySQL
Cuando me encontré con Core Data, yo estaba buscando el equivalente a una base de datos MySQL. Core Data es un framework para manejar la persistencia de datos. Según Apple, Core Data te permite ahorrar mucho código para manejar tu modelo de datos a la hora de guardar, eliminar, recuperar datos de tu modelo. No digo que no sea verdad pero como verás más adelante, utilizar Core Data puede no ser tan intuitivo como me gustaría.
Core Data Stack
El stack de Core Data es el nombre que se le da al código generado por Xcode que consiste en crear una serie de objetos que nos permitirán guardar y recuperar información de Core Data.
Existen cuatro clases que es imprescindible conocer cuando utilizas Core Data. Las cuatro clases conforman nuestro Core Data Stack.
NSManagedObjectModel
Es el modelo de datos. Representa cada objeto de nuestro modelo con sus propiedades, relaciones, … Para dejarlo claro, es como el esquema de una base de datos de MySQL.
En el proyecto de Xcode, tendrás un archivo con la extensión “.xcdatamodeld” que contendrá el modelo de datos. Aquí se pueden crear entidades con atributos y relaciones entre ellas.
NSPersistentStore
Es el almacenamiento que utiliza nuestro stack. Se pueden escoger cuatro tipos diferentes de almacenamiento:
- NSQLiteStoreType: Una base de datos SQLite. Es la opción más utilizada y seleccionada por defecto.
- NSXMLStoreType: Un archivo XML. Sólo se puede utilizar para OS X, así que tampoco me interesa.
- NSBinaryStoreType: Un archivo binario de datos. Tanto este tipo como el XML, son muy poco utilizados porque requieren ser cargados completamente en memoria antes de poder utilizarlos, lo cual requiere un consumo de recursos poco eficiente.
- NSInMemoryStoreType: Un almacenamiento en memoria. Lo curioso aquí es que este tipo de almacenamiento no persiste si cierras tu app o apagas tu dispositivo. Por lo tanto, no es de mucha utilidad.
NSPersistentStoreCoordinator
Esta clase hace de intermediaria entre el modelo de datos (NSManagedObjectModel) y el almacenamiento (NSPersistentStore). Permite por ejemplo abstraerte del tipo de almacenamiento seleccionado. Hace las funciones de un ORM, me recuerda a Doctrine por ejemplo que me permite abstraer el tipo de base de datos que estoy utilizando.
NSManagedObjectContext
Esta clase es la más importante y la clase con la que trabajarás día a día constantemente. Representa el contexto en el que estás trabajando. Todas tus operaciones de lectura y escritura se desarrollan en el contexto. Los cambios que realices en el contexto no serán almacenados en disco hasta que hagas una llamada al método “save()”.
Un objeto está fuertemente ligado a un contexto y no puede cambiar de contexto a lo largo de su ciclo de vida. Esto se convertirá en algo muy a tener en cuenta cuando utilices en tu app múltiples hilos de ejecución y necesites acceder a un mismo objeto desde ellos y modificarlo.
Cuatro operaciones básicas: Insertar, Recuperar, Modificar y Eliminar
Voy a crear un proyecto de pruebas de Xcode para mostrar como realizar las cuatro operaciones básicas. Me voy a saltar todos los pasos previos de crear proyecto, seleccionar Core Data, … Todo esto, lo podrás conseguir tú sólo estoy seguro y sino, no dudes en preguntar en los comentarios.
Modelo de datos
Sin complicaciones, mi modelo de datos se compone de 2 entidades con una relación entre ellas.
- Entidad: Persona
- nombre – String
- edad – Integer 32
- Entidad: Marca
- marca – String
- ano_compra – Integer 32
- Relación: Una persona puede ser propietaria de muchas coches (To Many) y un coche sólo tiene un propietario (To One).
Insertar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func insertar() { // Insertar una persona. // Crear un tipo de entidad: Persona. let personaEntity = NSEntityDescription.entityForName("Persona", inManagedObjectContext: managedContext) // Crear una entidad del tipo: personaEntity o sea Persona. let persona = Persona(entity: personaEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. persona.nombre = "Miguel" persona.edad = 32 // Insertar un coche. // Crear un tipo de entidad: Coche. let cocheEntity = NSEntityDescription.entityForName("Coche", inManagedObjectContext: managedContext) // Crear una entidad del tipo: cocheEntity o sea Coche. let coche = Coche(entity: cocheEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. coche.marca = "Ferrari" coche.ano_compra = 2045 // Relacionar el propietario con el coche. coche.propietario = persona // Guardar en disco. do { try managedContext.save() } catch let error as NSError { print("Error al insertar: \(error)") } } |
El código no es muy complicado pero intuitivo, intuitivo no es. Primero, debes insertar en el contexto una nueva entidad del tipo que necesitas. Por eso, necesitas el primer paso para crear el tipo de entidad que necesitas, luego insertas un nuevo NSManagedObject de ese tipo. Finalmente, debes llamar al método «save()» para que tu contexto termine guardando los cambios en disco, sino ninguno de tus cambios permanecerá en el siguiente arranque de la app.
Recuperar
El siguiente paso es recuperar los datos de esa persona que acabo de crear y su coche. Así de paso, compruebo que he insertado algo realmente en la base de datos y no estoy haciendo esto para nada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func recuperarPersona(nombre: String?) -> Persona? { let fetchRequest = NSFetchRequest(entityName: "Persona") if let nombre = nombre { fetchRequest.predicate = NSPredicate(format: "nombre == %@", nombre) } do { let results = try managedContext.executeFetchRequest(fetchRequest) if results.count > 0 { let p = results.first! as! Persona print("Encontrada una persona: \(p.nombre!) - \(p.edad!) con \(p.coches!.count) coche.") if let coches_de_p = p.coches { for c in coches_de_p { let c = c as! Coche print("Coche de \(p.nombre!): \(c.marca!) - \(c.ano_compra!)") } } return p } else { print("No hay personas.") return nil } } catch let error as NSError { print("Error al recuperar: \(error)") } return nil } |
Parece que sí que hay datos por lo tanto, está funcionando todo correctamente.
Para recuperar información, he utilizado NSFetchRequest con un simple predicado. No voy a entrar en detalles en este artículo con esta clase pero es donde reposa toda la dificultad y la utilidad de Core Data. Con esta clase, podrás recuperar la información de tu base de datos. Tiene muchas posibilidades, podrás utilizar predicados con NSPredicate para filtrar tus datos, podrás ordenarlos utilizando NSSortDescriptor, limitar la cantidad de datos, …
En otro artículo, trataré de hablar de todo lo que se puede hacer con NSFetchRequest. Bueno, quizás no todo, pero sí muchas cosas.
Modificar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func modificar(p: Persona) { // Cambiar los datos de la Persona p. p.nombre = "Manolo" p.edad = 23 // Insertar un nuevo coche. // Crear un tipo de entidad: Coche. let cocheEntity = NSEntityDescription.entityForName("Coche", inManagedObjectContext: managedContext) // Crear una entidad del tipo: cocheEntity o sea Coche. let coche = Coche(entity: cocheEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. coche.marca = "Seat" coche.ano_compra = 1989 // Relacionar el propietario con el coche. coche.propietario = p do { try managedContext.save() } catch let error as NSError { print("Error al modificar: \(error)") } } |
Posiblemente, de lo más sencillo, una vez que tienes el objeto en una variable. Modificarlo es coser y cantar.
Eliminar
1 2 3 4 5 6 7 8 9 10 |
func eliminar(p: Persona) { managedContext.deleteObject(p) do { try managedContext.save() } catch let error as NSError { print("Error al eliminar: \(error)") } } |
Igual que con modificar, si tienes el objeto recuperado, eliminarlo es muy sencillo. Puedes ver en la consola que ya no hay personas en la base de datos, por lo tanto ha sido eliminada correctamente.
Una simple introducción
Core Data es mucho más complejo de lo que acabo de mostrarte en este artículo. Si quieres utilizarlo, tendrás que investigar NSFetchRequest, NSPredicate, … Acabarás trabajando con NSFetchedResultsController y NSAsynchronousFetchRequest. Todos estos temas son lo suficientemente amplios como para ocupar un post entero cada uno, así que hablaré de ellos más adelante.
A continuación, os dejo el código completo de mi ViewController.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
import UIKit import CoreData class ViewController: UIViewController { var managedContext: NSManagedObjectContext! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // Insertar persona y coche. insertar() // Recuperar persona. if let p = recuperarPersona("Miguel") { // Modificar persona. modificar(p) } // Recuperar persona modificada. if let p_modificada = recuperarPersona("Manolo") { // Eliminar persona modificada. eliminar(p_modificada) } // Recuperar persona eliminada, o sea no obtendrás nada. let _ = recuperarPersona("Manolo") } func insertar() { // Insertar una persona. // Crear un tipo de entidad: Persona. let personaEntity = NSEntityDescription.entityForName("Persona", inManagedObjectContext: managedContext) // Crear una entidad del tipo: personaEntity o sea Persona. let persona = Persona(entity: personaEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. persona.nombre = "Miguel" persona.edad = 32 // Insertar un coche. // Crear un tipo de entidad: Coche. let cocheEntity = NSEntityDescription.entityForName("Coche", inManagedObjectContext: managedContext) // Crear una entidad del tipo: cocheEntity o sea Coche. let coche = Coche(entity: cocheEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. coche.marca = "Ferrari" coche.ano_compra = 2045 // Relacionar el propietario con el coche. coche.propietario = persona // Guardar en disco. do { try managedContext.save() } catch let error as NSError { print("Error al insertar: \(error)") } } func recuperarPersona(nombre: String?) -> Persona? { let fetchRequest = NSFetchRequest(entityName: "Persona") if let nombre = nombre { fetchRequest.predicate = NSPredicate(format: "nombre == %@", nombre) } do { let results = try managedContext.executeFetchRequest(fetchRequest) if results.count > 0 { let p = results.first! as! Persona print("Encontrada una persona: \(p.nombre!) - \(p.edad!) con \(p.coches!.count) coche.") if let coches_de_p = p.coches { for c in coches_de_p { let c = c as! Coche print("Coche de \(p.nombre!): \(c.marca!) - \(c.ano_compra!)") } } return p } else { print("No hay personas.") return nil } } catch let error as NSError { print("Error al recuperar: \(error)") } return nil } func modificar(p: Persona) { // Cambiar los datos de la Persona p. p.nombre = "Manolo" p.edad = 23 // Insertar un nuevo coche. // Crear un tipo de entidad: Coche. let cocheEntity = NSEntityDescription.entityForName("Coche", inManagedObjectContext: managedContext) // Crear una entidad del tipo: cocheEntity o sea Coche. let coche = Coche(entity: cocheEntity!, insertIntoManagedObjectContext: managedContext) // Asignar valores. coche.marca = "Seat" coche.ano_compra = 1989 // Relacionar el propietario con el coche. coche.propietario = p do { try managedContext.save() } catch let error as NSError { print("Error al modificar: \(error)") } } func eliminar(p: Persona) { managedContext.deleteObject(p) do { try managedContext.save() } catch let error as NSError { print("Error al eliminar: \(error)") } } } |