10 principios básicos para utilizar CoreData sin volarse la cabeza

o 10 batallas lecciones aprendidas de CoreData

Olivier Destrebecq
Olivier Destrebecq

Sigue

1 de mayo, 2020 – 7 min read

He estado utilizando CoreData a lo largo de múltiples proyectos durante mi carrera como desarrollador de la plataforma de Apple (la primera vez fue en MacOS 10.5…), y a lo largo de los años he aprendido cosas de la manera más difícil (aka: «golpeando mi cabeza contra la pared durante demasiado tiempo»). Así que pensé en compartir mi aprendizaje con ustedes. Con suerte, te ahorraré unos cuantos moratones en la frente.

Nota que esto no es una guía de cómo configurar CoreData. Pretende ser un artículo con algunas buenas prácticas que puedes recoger para tus proyectos.

Aquí está el resumen rápido de Apple: «Utiliza Core Data para guardar los datos permanentes de tu aplicación para su uso sin conexión, para almacenar en caché los datos temporales y para añadir la funcionalidad de deshacer tu aplicación en un único dispositivo».

Para dar un poco más de detalle, CoreData es la tecnología de Apple para guardar tus datos estructurados localmente. Utilizas el editor de modelos para definir tu estructura de datos al igual que un editor de esquemas de bases de datos, luego puedes utilizar esos objetos en tu código para almacenar tus datos.

CoreData es un marco de gestión de gráficos de objetos

Lo que esto significa es que cuando creas tu modelo, defines tus objetos y sus relaciones. Por ejemplo, si tienes una entidad Empresa y una entidad Empleado en tu modelo de datos, esas dos entidades estarán vinculadas por una relación que las une. Probablemente se llamará employees en la entidad Company y employer en la entidad Employee. Simplemente añades la propiedad de relación en las entidades y ya está. No es necesario crear una tabla de unión como en una base de datos.

La belleza de CoreData en comparación con una base de datos tradicional es que cuando usted consulta para obtener la empresa X, usted será capaz de obtener los empleados directamente simplemente accediendo a company.employees. No es necesario construir una consulta join o volver a consultar.

CoreData puede almacenar los datos en diferentes formatos

Sí, has oído bien, CoreData puede guardar tus datos en diferentes formatos/lugares. Cada uno tiene sus pros y sus contras.

  1. Almacenamiento binario
  2. Almacenamiento SQLite
  3. Almacenamiento en memoria

Puedes leer este artículo para conocer los pros y los contras de cada uno. Yo siempre uso SQLite, pero podría ver cómo un almacén en memoria sería súper rápido.

Un par de definición de términos

NSManagedObject: es la clase base que representa los objetos que tienes almacenados en CoreData.

Fetch request: es el equivalente a una consulta en el mundo de CoreData. Está representada por la clase NSFetchedObject.

Use NSPersistentContainer

Hasta iOS 10.0, la configuración de la pila de CoreData implicaba bastante código boilerplate. Tenías que arreglártelas por ti mismo para definir la estrategia a utilizar para lidiar con el uso multihilo de CoreData.

Con NSPersistentContainer, Apple encapsula la mayor parte del código boilerplate. Ahorrándote el debate sobre cómo configurar mejor tu pila para un rendimiento óptimo.

Este artículo contiene toda la información que necesitas para usarlo correctamente.

Usa relaciones

Siempre menciono esto porque en dos ocasiones, para proyectos que heredé, el modelo CoreData estaba configurado como una base de datos. No se definieron relaciones. En su lugar, se almacenaba el ID del objeto relacionado y, por tanto, se requería una petición de fetch extra para obtener el objeto relacionado. Creo que muchos desarrolladores abordan CoreData como una base de datos y cometen este error. Esto acaba haciendo todo el proyecto mucho más complejo y mucho más lento.

Sigue este artículo para empezar.

Evita el código stringly en la medida de lo posible

El código stringly se refiere al código que utiliza cadenas para acceder a los datos. En iOS, tienes unas cuantas APIs de stringly: UserDefaults, CoreData son algunas que me vienen a la mente.

Si volvemos al ejemplo de la empresa, a continuación hay dos opciones para crear una empresa y establecer su nombre:

//stringly version to create and edit a company
let company = NSEntityDescription.insertNewObject(forEntityName: "Company", in: managedObjectContext)
company.set("my less than great corporation", forKey: "name")//non stringly version
let company = Company(in: managedObjectContext)
company.name = "My super corporation"

A primera vista, ambas opciones funcionan, compilan, no hay nada malo en ellas…

El problema con el código stringly es que el compilador no puede ayudarte. Imagínese que usted necesita para acceder a la name de la empresa en varios lugares en el código, y, siendo realistas, usted necesitará al menos dos: uno para establecer el y uno para leerlo, ahora tiene que escribir que name cadena en dos lugares. Seguro que puedes crear una variable para almacenar la cadena y usar esa variable, pero eso requiere un esfuerzo extra, no será tecleado comprobado y todos sabemos que los desarrolladores son perezosos…

Así que eso normalmente deja esa cadena en múltiples lugares. Luego quieres renombrarla y tienes que cazarla con buscar y reemplazar… Si se te escapa una, tu aplicación se colgará.

Aquí está la lista de características del compilador de las que no te beneficiarás con la sintaxis stringly:

  • autocompletar
  • refactor
  • advertencia del compilador en caso de error ortográfico
  • error del compilador si eliminas/renombramos esta entidad o propiedad en tu modelo

Para poder utilizar la versión nostringly del código simplemente tendrá que pedir al compilador que genere clases para sus entidades (puede seguir las instrucciones de Apple en «Seleccionar una opción de generación de código»). Esto es ahora el valor por defecto cuando se crea una nueva Entidad.

Mantén tu NSPersitentContainer.viewContext de sólo lectura

Por razones de rendimiento, no quieres realizar operaciones largas en el viewContext ya que se ejecutará en el hilo principal y potencialmente bloqueará la UI.

La solución es acostumbrarse a hacer modificaciones a tus objetos sólo dentro de un bloque que programes con el método NSPersistentContainer.performBackgroundTask(). Cuando guardes tus cambios en ese bloque, éstos se fusionarán de nuevo en NSPersitentContainer.viewContext y podrás actualizar tu UI.

Cuidado con esos hilos

Una de las principales pegas de CoreData es utilizarlo de forma segura para los hilos. Aunque una vez que se entiende el concepto, es bastante fácil.

Lo que se reduce a que cualquier objeto de datos que se obtiene de CoreData sólo se puede utilizar en el hilo al que está unido el managedobjectContext del objeto.

Una forma de asegurarte de que estás usando el hilo correcto para acceder a tu objeto es usar el NSManagedObjectContext.performBlockAndWait que asegurará que tu código se usa en el hilo correcto.

Una vez en ese bloque, tendrás que usar NSManagedObject.objectID y NSManagedObjectContext.existingObject(withId:) para pasar el objeto a través de los hilos y el contexto.

Si no haces esto, lo más probable es que acabes con bloqueos. Imagina:

  • Programas una tarea desde el hilo principal con En el caso de que algo programado desde el hilo principal NSManagedObjectContext.performBlockAndWait(). En este momento, el hilo principal está bloqueado esperando que se complete este bloque.
  • Dentro de este bloque, pasas el objeto foo del contexto principal al método bar() y lo despachas a otro hilo.
  • Para ser un buen ciudadano de CoreData bar() obtendrá el contexto adjunto a foo y despachará su tarea usando foo.managedObjectContext.performBlockAndWait()
  • Boom, estás en punto muerto ya que este contexto sólo se ejecuta en el hilo principal y el hilo principal está esperando a que se complete.

Otra convención que ayuda es pasar objetos entre controladores de vista sólo si existen en el contexto del hilo principal.

Usa el argumento de lanzamiento para rastrear el contexto y el hilo

Si sigues los consejos anteriores, esto debería limitar las posibilidades de que una instancia NSManagedObject sea llamada en el hilo equivocado. Pero esto todavía puede suceder. Así que asegúrese de que cuando usted está en el desarrollo para activar la aserción de depuración de hilos

-com.apple.CoreData.ConcurrencyDebug 1

Este artículo le dirá exactamente cómo hacerlo y le dirá acerca de otras banderas que puede configurar para más depuración.

Utilizar consultas agregadas

CoreData ofrece groupBy, sum, y count como funciones agregadas que se pueden ejecutar durante una consulta. La configuración puede ser un poco compleja, pero vale la pena.

La alternativa es crear una consulta para obtener todos los objetos que coinciden con sus criterios y luego ejecutar un bucle for a través de todas las entidades y hacer el cálculo usted mismo. Más fácil de codificar, pero mucho menos eficiente.

Cuando se ejecuta una solicitud de obtención agregada, CoreData la traduce en una consulta de base de datos y ejecuta el cálculo directamente en el nivel de la base de datos. Esto hace que sea súper rápido.

Este artículo es una buena introducción a cómo hacerlo.

Mantén el código relacionado con los datos del núcleo en categorías

Una vez que tengas tu subclase NSManagedObject creada por Xcode, lo más probable es que te encuentres con que tienes código que debería estar en la clase que representa tu objeto (para calcular valores, importar datos de tu servidor, etc). Pero has pedido a CoreData que cree las clases por ti…

Así que tendrás que crear una categoría para el objeto que necesita un comportamiento extra. Sólo pon el código relacionado con CoreData ahí para que puedas mantenerlo limpio.

Adopta un modelo de transacción para tus cambios en tus datos

Puedes modificar tus objetos todo lo que quieras, pero para persistir esos cambios necesitas llamar a NSManagedObjectContext.save().

Te encontrarás rápidamente con que te olvidas de llamar a save() y tendrás algunos cambios colgando en tu contexto. En este punto todas las apuestas están fuera, la próxima vez que llames a save(), no estarás seguro de lo que estás confirmando.

Adopta un modelo de transacción para evitar dejar un contexto sucio. Para ello, crea una categoría en NSPersistentContainer así:

extension NSPersistentContainer {
@objc func performBackgroundSaveTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
performBackgroundTask { (context: NSManagedObjectContext) in
block(context)
context.save()
}
}
}

Asegúrate a partir de ahora de que cualquier cambio que hagas se ejecute en un bloque programado con performBackgroundSaveTask(). Así te garantizas que los cambios se guarden cuando los hagas.

No guardes el contexto desde dentro de un método de categoría de modelo

Nunca llames a save() desde dentro de una categoría de entidad. No hay nada peor que llamar a un método en su objeto sin darse cuenta de que va a guardar todo el contexto con él. Comprometiendo algunos cambios que tal vez no quieras guardar. Llamar a guardar debe hacerse en el punto donde se inician las modificaciones del objeto.