Streaming is available in most browsers,
and in the Developer app.
-
What’s new in Core Data
Elevate your app's data persistence with improvements in Core Data. Learn how you can use composite attributes to create more intuitive data models. We'll also show you how to migrate your schema through disruptive changes, when to defer intense migrations, and how to avoid overhead on a person's device. To get the most out of this session, you should be familiar with handling different data types in Core Data as well as the basics of lightweight migration.
Chapters
- 0:00 - Intro
- 0:56 - Composite attributes
- 6:31 - Stage your migrations
- 18:23 - Defer your migrations
- 22:33 - Wrap-up
Resources
Related Videos
WWDC22
-
Download
♪ ♪ David: Hi, and welcome to “What's New in Core Data.” My name is David Stites, and I'm an engineer on the Core Data team. In this session, you'll learn about new technologies in Core Data that will help you more quickly and easily design, query, update, and migrate the Core Data data model in your app.
I'll start by talking about composite attributes, a great new way to organize structured data in your app's model, before talking about how to “stage” your most complex model migrations so that you can use lightweight migration, and I'll finish up with how to defer your model migrations to keep your app responsive. Composite attributes are a new type of attribute.
Composite attributes allow for the encapsulation of complex and custom data types within a single attribute. Each composite attribute is composed of attributes of the built-in Core Data types that you're already familiar with, such as String, Float, Int, and Data.
Composite attributes may be nested within each other so that a top-level composite attribute may contain additional composite attributes.
The Xcode Core Data model editor has been updated to make it easy to define and manage your model's composite attributes.
Composite attributes are a compelling alternative to using transformable type attributes to create durable custom data types. There is no need to write code that transforms the attribute's value. Unlike transformable attributes, composite attributes allow NSFetchRequests with NSPredicates configured with the composite attribute's namespaced keypaths.
Composite attributes can be used to encapsulate a proliferation of flattened attributes, leading to more maintainable and readable code.
Composite attributes can be used to improve the performance of your app. If your data model is composed in such a way that fetching one entity almost always results in accessing a relationship to another entity, you can refactor that relationship into using composite attributes. The effect of embedding a composite attribute in the first entity is that it prevents faulting in objects across the relationship.
The composite attribute class is NSCompositeAttributeDescription. The attribute type for an NSCompositeAttributeDescription is NSCompositeAttributeType.
The NSCompositeAttributeDescription class contains an array, elements, that consists of NSAttributeDescription's or other nested NSCompositeAttributeDescription's.
The elements array cannot contain other types of property descriptions, such as NSRelationshipDescription. Attempting to set invalid elements will result in an NSInvalidArgumentException.
I'm gonna describe to you how to adopt composite attributes with a demo.
Consider this basic data model with an Aircraft entity. It has a number of attributes, including a colors attribute, which is a transformable type. The transformer for that type stores and parses a formatted string that describes the primary, secondary, and tertiary colors of the aircraft.
I'll improve this entity by replacing the colors attribute with a composite attribute colorScheme to store the paint colors of the aircraft.
colorScheme is a composite attribute with the elements: Primary, secondary, and tertiary, each of which are a String attribute.
In Xcode, I'll open a project which is an app I use to track my flight time.
The data model for that app is configured with the Aircraft entity that I just talked about, as well as a couple other entities.
To begin the conversion, in the Core Data model editor, I am adding a new composite attribute named colorScheme.
Within that composite, I am adding three string attributes, primary, secondary, and tertiary.
In the Aircraft entity, I'll add the composite attribute and set the type of that attribute to colorScheme.
The work in the model is now complete, and it's time to update the code.
In my Aircraft implementation, I am adding a new property, @NSManaged var colorScheme, whose type is a Dictionary with a String key and Any object. As I use this composite attribute throughout the code, I am accessing the values using dictionary notation, with the attribute's name as the key. Here, I am setting the colorScheme attribute of the aircraft by using the String keys primary, secondary, and tertiary.
Similarly, when I configure a NSFetchRequest with a NSPredicate, the elements of a composite attribute are accessed via a namespaced keypath. Here, colorScheme.primary is used to filter on that attribute.
As an application evolves, it may become necessary to change the data model.
Updating the data model requires that those changes are materialized in the underlying storage schema.
If a numPassengers attribute is added to the model, the corresponding storage must be updated.
The process of performing schema changes is called migration.
After migration, the changes are fully reflected in the underlying storage.
Core Data has a built-in migration toolset to help keep an app's data storage up to date with the current data model. Collectively, these tools are referred to as “lightweight migration.” To learn more about lightweight migration, watch "Evolve your app's schema" from WWDC 2022.
Sometimes, the combined changes to a data model exceed the capabilities of lightweight migration. The solution to this problem is a staged migration.
The staged migrations API was designed with a couple goals in mind: Help you migrate complex data models that have non-conforming lightweight schema changes, simplify your app by potentially removing thousands of lines of code related to migrations and the migration infrastructure, and provide opportunities for your app to gain execution control during the migration process to perform various tasks.
To use this API, there are several steps you'll need to take: Determine when changes to your model do not conform to operations supported by lightweight migration, decompose non-conforming model changes into a series of conforming model changes that are supported by lightweight migration, describe a total ordering of NSManagedObjectModel's to Core Data using the new staged migrations APIs, and have Core Data execute an event loop that iteratively steps through each unprocessed model in a serial order and migrate the store. At certain points during the migration, execution control will be given to your app to perform any necessary tasks related to that migration.
To determine when your model has non-conforming lightweight changes, you have several options. The first option is to review the schema changes manually and ensure each change is eligible for lightweight migration.
The second option is to try to open the persistent store with the new model and the lightweight migration options, NSMigratePersistentStores AutomaticallyOption and NSInferMappingModelAutomaticallyOption set to true. If the changes are not lightweight-eligible, you'll receive an NSPersistentStore IncompatibleVersionHashError.
The final option is to use NSMappingModel.inferredMappingModel (forSourceModel:destinationModel:). This method returns the inferred model if Core Data was able to create it. Otherwise, it returns nil.
Considering the Aircraft model again, it has a new attribute, flightData, that stores data in a binary format.
Suppose there's a need to denormalize this model and separate all flight data into its own entity type, all while preserving any existing data and the relationship to the aircraft from which it was generated. This is a very complex model change and is not eligible for lightweight migration by itself. These changes need to be decomposed to use staged migration. When decomposing non-lightweight changes, the goal is to transform migration tasks that aren't eligible for lightweight migration into a minimum series of migrations that are eligible for lightweight migration.
Each of the models introduced will have one or more operations that is within the capabilities of lightweight migration that compose the non-conforming changes. The result is a series of migrations where each model is lightweight migratable but equivalent to the non-conforming migration.
Returning to the example, I've labeled the original model ModelV1. This model migration will be decomposed by introducing two new model versions, ModelV2 and ModelV3. In ModelV2, the Aircraft entity gains a relationship called flightParameters, which is a collection of newly created FlightData entities. The FlightData entity has a binary type attribute data and a relationship to an Aircraft. To preserve the existing data, the migration stage will copy the data from the Aircraft entity over into the new FlightData entities and relate them to the Aircraft.
Our final model is ModelV3, created from ModelV2. In ModelV3, the old flightData attribute is deleted from Aircraft entity, and the model is successfully denormalized, and all existing data is preserved. Each of the steps described is within the capabilities of lightweight migration.
To describe a total ordering of models, the Core Data framework level support consists of the following classes: NSStagedMigrationManager, NSCustomMigrationStage, NSLightweightMigrationStage, and NSManagedObjectModelReference.
The NSStagedMigrationManager class encapsulates a total ordering of NSCustomMigrationStage's and the supplementary NSLightweightMigrationStage's that is described by you. The staged migration manager also manages the migration event loop and provides access to the migrating store via an NSPersistentContainer. The manager is added to the store options using the key NSPersistentStoreStagedMigrationManager OptionKey.
Migration stages form the basis for migrating between versions of a model.
As you adopt staged migration, you'll describe each model version to Core Data using either an NSCustomMigrationStage or an NSLightweightMigrationStage. The NSLightweightMigrationStage class describes a series of models that did not require decomposition and that were lightweight migration eligible. This will likely be the majority of your models. These lightweight migration stages are used to supplement the total ordering of models described to Core Data. All lightweight model versions must be represented in one or more NSLightweightMigrationStage's.
Each decomposed version of your model you create will be represented using an NSCustomMigrationStage and contain a source model reference and a destination model reference.
NSCustomMigrationStage provides optional handlers that run immediately prior to and after the migration stage. These handlers give you the ability to run custom code during the migration process.
Staged migrations make use of the NSManagedObjectModelReference class. This class represents a promise of an NSManagedObjectModel. During migration, Core Data will fulfill this promise. An NSManagedObjectModelReference is flexible and can be created in a number of different ways.
Every NSManagedObjectModelReference needs to be initialized with a version checksum. This is to validate the model hasn't inadvertently changed. The checksum can be obtained using the NSManagedObjectModel .versionChecksum method.
Alternatively, you can retrieve the version checksum from the Xcode build log under “Compile data model.” Search for the string “version checksum.” For versioned models, the checksum is also available in the VersionInfo.plist of the NSManagedObjectModel bundle.
Returning to the example, to start using staged migration, I'll begin by creating model references for each of the three models. I am using the initializer that accepts a model name and bundle reference, but there are other options as well.
The next step is to describe the required migration stages. Since the first stage only added the flightData attribute, that can be represented in a lightweight stage, as adding attributes is a lightweight change.
The next stage, however, will be a custom stage because the model changes were decomposed into two model versions, and we need to run custom code to preserve existing data. The custom migration stage is initialized with ModelV2 and ModelV3.
In the willMigrateHandler, the code fetches entity rows where flightData is not nil. The generic NSManagedObject and NSFetchRequestResult types are being used instead of the Aircraft managed object subclass due to the fact that it is possible that the Aircraft class may not exist as expected during the migration.
For each fetched Aircraft entity, the data is copied into a new instance of FlightData, and the two entities are related and persisted. At the end of the execution of this migration stage, the store schema is updated to the latest model, and the existing data has been preserved.
To finish the staged migration, I create an NSStagedMigrationManager with the lightweight migration stage and the custom migration stage.
The NSStagedMigrationManager is added to the NSPersistentStoreDescription options with the key NSPersistentStore StagedMigrationManagerOptionKey.
The persistent stores are then loaded to start the migration process and affect the store schema. And that's it. Core Data will automatically apply the required stages and migrate the store schema.
Some lightweight migrations require additional runtime that your app may not be able to provide in the foreground.
The process of transforming user data during lightweight migration is not instantaneous. For example, if the migration involves copying data from one column to another, or one table to another, it may take some time. This can result in a frustrating user experience, especially if the migration is done at launch time.
Deferred migration can help you solve this problem. This API will allow you to defer some of the work done during lightweight migration with the ability to finish the deferred work at a later date. During a lightweight migration, if an entity has a migration transformation that requires clean up, such as updating indices or dropping a column after performing a table copy, this table transformation can be delayed until you deem that the resources are available to perform the table transformation. The lightweight migration is still synchronous and occurs normally. Only the clean up of the schema is deferred. Your app will use the latest schema as normal. To opt into deferred migration, set the NSPersistentStore DeferredLightweightMigrationOptionKey in the store options to true.
The deferred migration API has runtime compatibility all the way back to macOS Big Sur and iOS 14.
Deferred migration is only available for SQLite store types.
Some examples of where deferred migration might be useful include: Removing attributes or relationships from an entity, changing relationships where an entity hierarchy no longer exists, and changing relationships from being ordered to non-ordered.
To finish deferred migration tasks, check the persistent store metadata. If it contains the key NSPersistentStore DeferredLightweightMigrationOptionKey, that is a signal to you that there is deferred migration work that needs to be finished. The delayed migrations can be processed by invoking NSPersistentStoreCoordinator .finishDeferredLightweightMigration.
To defer any lightweight migration in your app, set the NSPersistentStoreDeferred LightweightMigrationOptionKey to true in your store options when adding the persistent store to the coordinator. When it's a good time to finish a deferred migration, you can check to see if there is pending deferred work by checking the metadata for the store. If NSPersistentStoreDeferredLightweight MigrationOptionKey is set to true, then call finishDeferredLightweightMigration().
To schedule your deferred migration tasks, consider using the Background Tasks API. BGProcessingTask is meant for time-consuming operations such as long data updates and app maintenance. The system will determine the best time to run your task. However, generally processing tasks only run on the device when it's idle and will terminate any background processing tasks when the user starts using the device.
Deferred and staged migration can be combined. If you have a set of complex migrations that may take a while, consider designing stages that take advantage of both APIs capabilities. Returning to the example model, in ModelV3, where we remove the attribute flightData, this might make for a good deferred migration candidate.
There are three great new technologies in Core Data. Encapsulate your custom data types in a nestable, structured way using composite attributes, perform complex model migrations using staged migration by decomposing your model changes, and turbocharge your app's performance by delaying some migration work using deferred migration. All three technologies work in harmony to improve your app.
Our team is excited to hear how you use these new technologies. Thanks for watching, and have a great WWDC. ♪ ♪
-
-
5:39 - Adding a composite attribute
enum PaintColor: String, CaseIterable, Identifiable { case none, white, blue, orange, red, gray, green, gold, yellow, black var id: Self { self } } extension Aircraft { @nonobjc public class func fetchRequest() -> NSFetchRequest<Aircraft> { return NSFetchRequest<Aircraft>(entityName: "Aircraft") } @NSManaged public var aircraftCategory: String? @NSManaged public var aircraftClass: String? @NSManaged public var aircraftType: String? @NSManaged public var colorScheme: [String: Any]? @NSManaged public var photo: Data? @NSManaged public var tailNumber: String? @NSManaged public var logEntries: NSSet? }
-
5:53 - Setting a composite attribute
private func addAircraft() { viewContext.performAndWait { let newAircraft = Aircraft(context: viewContext) newAircraft.tailNumber = tailNumber newAircraft.aircraftType = aircraftType newAircraft.aircraftClass = aircraftClass newAircraft.aircraftCategory = aircraftCategory newAircraft.colorScheme = [ "primary": primaryColor.rawValue, "secondary": secondaryColor.rawValue, "tertiary": tertiaryColor.rawValue ] do { try viewContext.save() } catch { // ... } } }
-
6:11 - Fetching a composite attribute
private func findAircraft(with color: String) { viewContext.performAndWait { let fetchRequest = Aircraft.fetchRequest() fetchRequest.predicate = NSPredicate(format: "colorScheme.primary == %@", color) do { var fetchedResults: [Aircraft] fetchedResults = try viewContext.fetch(fetchRequest) // ... } catch { // Handle any errors that may occur } } }
-
16:00 - Creating managed object model references for staged migration
let v1ModelChecksum = "kk8XL4OkE7gYLFHTrH6W+EhTw8w14uq1klkVRPiuiAk=" let v1ModelReference = NSManagedObjectModelReference( modelName: "modelV1" in: NSBundle.mainBundle versionChecksum: v1ModelChecksum ) let v2ModelChecksum = "PA0Gbxs46liWKg7/aZMCBtu9vVIF6MlskbhhjrCd7ms=" let v2ModelReference = NSManagedObjectModelReference( modelName: "modelV2" in: NSBundle.mainBundle versionChecksum: v2ModelChecksum ) let v3ModelChecksum = "iWKg7bxs46g7liWkk8XL4OkE7gYL/FHTrH6WF23Jhhs=" let v3ModelReference = NSManagedObjectModelReference( modelName: "modelV3" in: NSBundle.mainBundle versionChecksum: v3ModelChecksum )
-
16:19 - Creating migration stages for staged migration
let lightweightStage = NSLightweightMigrationStage([v1ModelChecksum]) lightweightStage.label = "V1 to V2: Add flightData attribute" let customStage = NSCustomMigrationStage( migratingFrom: v2ModelReference, to: v3ModelReference ) customStage.label = "V2 to V3: Denormalize model with FlightData entity"
-
16:54 - willMigrationHandler and didMigrationHandler of NSCustomMigrationStage
customStage.willMigrateHandler = { migrationManager, currentStage in guard let container = migrationManager.container else { return } let context = container.newBackgroundContext() try context.performAndWait { let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Aircraft") fetchRequest.predicate = NSPredicate(format: "flightData != nil") do { var fetchedResults: [NSManagedObject] fetchedResults = try viewContext.fetch(fetchRequest) for airplane in fetchedResults { let fdEntity = NSEntityDescription.insertNewObject( forEntityName: "FlightData, into: context ) let flightData = airplane.value(forKey: "flightData") fdEntity.setValue(flightData, forKey: “data”) fdEntity.setValue(airplane, forKey: "aircraft") airplane.setValue(nil, forKey: "flightData") } try context.save() } catch { // Handle any errors that may occur } } }
-
17:41 - Loading the persistent stores with an NSStagedMigrationManager
let migrationStages = [lightweightStage, customStage] let migrationManager = NSStagedMigrationManager(migrationStages) let persistentContainer = NSPersistentContainer( path: "/path/to/store.sqlite", managedObjectModel: myModel ) var storeDescription = persistentContainer?.persistentStoreDescriptions.first storeDescription?.setOption( migrationManager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey ) persistentContainer?.loadPersistentStores { storeDescription, error in if let error = error { // Handle any errors that may occur } }
-
21:01 - Adding a persistent store with NSPersistentStoreDeferredLightweightMigrationOptionKey option
let options = [ NSPersistentStoreDeferredLightweightMigrationOptionKey: true, NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true ] let store = try coordinator.addPersistentStore( ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options )
-
21:17 - Executing deferred migrations
// After using BGProcessingTask to run migration work let metadata = coordinator.metadata(for: store) if (metadata[NSPersistentStoreDeferredLightweightMigrationOptionKey] == true) { coordinator.finishDeferredLightweightMigration() }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.