Chapters

Hide chapters

Core Data by Tutorials

Eighth Edition · iOS 14 · Swift 5.3 · Xcode 12

Core Data by Tutorials

Section 1: 11 chapters
Show chapters Hide chapters

3. The Core Data Stack
Written by Pietro Rea

Until now, you’ve been relying on Xcode’s Core Data template. There’s nothing wrong with getting help from Xcode (that’s what it’s there for!). But if you really want to know how Core Data works, building your own Core Data stack is a must.

The stack is made up of four Core Data classes:

  • NSManagedObjectModel
  • NSPersistentStore
  • NSPersistentStoreCoordinator
  • NSManagedObjectContext

Of these four classes, you’ve only encountered NSManagedObjectContext so far in this book. But the other three were there behind the scenes the whole time, supporting your managed context.

In this chapter, you’ll learn the details of what these four classes do. Rather than rely on the default starter template, you’ll build your own Core Data stack; a customizable wrapper around these classes.

Getting started

The sample project for this chapter is a simple dog-walking app. This application lets you save the date and time of your dog walks in a simple table view. Use this app regularly and your pooch (and his bladder) will love you.

You’ll find the sample project DogWalk in the resources accompanying this book. Open DogWalk.xcodeproj, then build and run the starter project.

As you can see, the sample app is already a fully-working (albeit simple) prototype. Tapping on the plus (+) button on the top-right adds a new entry to the list of walks. The image represents the dog you’re currently walking, but otherwise does nothing.

The app has all the functionality it needs, except for one important feature: The list of walks doesn’t persist. If you terminate DogWalk and re-launch, your entire history is gone. How will you remember if you walked your pooch this morning?

Your task in this chapter is to save the list of walks in Core Data. If that sounds like something you’ve already done in Chapters 1 and 2, here’s the twist; you’ll be writing your own Core Data stack to understand what’s really going on under the hood!

Rolling your own Core Data stack

Knowing how the Core Data stack works is more than a nice to know. If you’re working with a more advanced setup, such as migrating data from an old persistent store, digging into the stack is essential.

Before you jump into the code, let’s consider what each of the four classes in the Core Data stack — NSManagedObjectModel, NSPersistentStore, NSPersistentStoreCoordinator and NSManagedObjectContext — does in detail.

Note: This is one of the few parts of the book where you’ll read about the theory before using the concepts in practice. It’s almost impossible to separate one component from the rest of the stack and use it in isolation.

The managed object model

The NSManagedObjectModel represents each object type in your app’s data model, the properties they can have, and the relationships between them. Other parts of the Core Data stack use the model to create objects, store properties and save data.

As mentioned earlier in the book, it can be helpful to think about NSManagedObjectModel as a database schema. If your Core Data stack uses SQLite under the hood, NSManagedObjectModel represents the schema for the database.

However, SQLite is only one of many persistent store types you can use in Core Data (more on this later), so it’s better to think of the managed object model in more general terms.

Note: You may be wondering how NSManagedObjectModel relates to the data model editor you’ve been using all along. Good question!

The visual editor creates and edits an xcdatamodel file. There’s a special compiler, momc, that compiles the model file into a set of files in a momd folder.

Just as your Swift code is compiled and optimized so it can run on a device, the compiled model can be accessed efficiently at runtime. Core Data uses the compiled contents of the momd folder to initialize an NSManagedObjectModel at runtime.

The persistent store

NSPersistentStore reads and writes data to whichever storage method you’ve decided to use. Core Data provides four types of NSPersistentStore out of the box: three atomic and one non-atomic.

An atomic persistent store needs to be completely deserialized and loaded into memory before you can make any read or write operations. In contrast, a non-atomic persistent store can load chunks of itself onto memory as needed.

Here’s a brief overview of the four built-in Core Data store types:

  1. NSSQLiteStoreType is backed by an SQLite database. It’s the only non-atomic store type Core Data supports out of the box, giving it a lightweight and efficient memory footprint. This makes it the best choice for most iOS projects. Xcode’s Core Data template uses this store type by default.

  2. NSXMLStoreType is backed by an XML file, making it the most human-readable of all the store types. This store type is atomic, so it can have a large memory footprint. NSXMLStoreType is only available on OS X.

  3. NSBinaryStoreType is backed by a binary data file. Like NSXMLStoreType, it’s also an atomic store, so the entire binary file must be loaded onto memory before you can do anything with it. You’ll rarely find this type of persistent store in real-world applications.

  4. NSInMemoryStoreType is the in-memory persistent store type. In a way, this store type is not really persistent. Terminate the app or turn off your phone, and the data stored in an in-memory store type disappears into thin air. Although this may seem to defeat the purpose of Core Data, in-memory persistent stores can be helpful for unit testing and some types of caching.

Note: Were you holding your breath for a persistent store type backed by a JSON file or a CSV file? Bummer. The good news is you can create your own type of persistent store by subclassing NSIncrementalStore.

Refer to Apple’s Incremental Store Programming Guide if you’re curious about this option:

https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/IncrementalStorePG/Introduction/Introduction.html

The persistent store coordinator

NSPersistentStoreCoordinator is the bridge between the managed object model and the persistent store. It’s responsible for using the model and the persistent stores to do most of the hard work in Core Data. It understands the NSManagedObjectModel and knows how to send information to, and fetch information from, the NSPersistentStore.

NSPersistentStoreCoordinator also hides the implementation details of how your persistent store or stores are configured. This is useful for two reasons:

  1. NSManagedObjectContext (coming next!) doesn’t have to know if it’s saving to an SQLite database, XML file or even a custom incremental store.

  2. If you have multiple persistent stores, the persistent store coordinator presents a unified interface to the managed context. As far as the managed context is concerned, it always interacts with a single, aggregate persistent store.

The managed object context

On a day-to-day basis, you’ll work with NSManagedObjectContext the most out of the four stack components. You’ll probably only see the other three components when you need to do something more advanced with Core Data.

Since working with NSManagedObjectContext is so common, understanding how contexts work is very important! Here are some things you may have already picked up from the book so far:

  • A context is an in-memory scratchpad for working with your managed objects.
  • You do all of the work with your Core Data objects within a managed object context.
  • Any changes you make won’t affect the underlying data on disk until you call save() on the context.

Now here are five things about contexts not mentioned before. A few of them are very important for later chapters, so pay close attention:

  1. The context manages the lifecycle of the objects it creates or fetches. This lifecycle management includes powerful features such as faulting, inverse relationship handling and validation.

  2. A managed object cannot exist without an associated context. In fact, a managed object and its context are so tightly coupled that every managed object keeps a reference to its context, which can be accessed like so:

let managedContext = employee.managedObjectContext
  1. Contexts are very territorial; once a managed object has been associated with a particular context, it will remain associated with the same context for the duration of its lifecycle.

  2. An application can use more than one context — most non-trivial Core Data applications fall into this category. Since a context is an in-memory scratch pad for what’s on disk, you can actually load the same Core Data object onto two different contexts simultaneously.

  3. A context is not thread-safe. The same goes for a managed object: You can only interact with contexts and managed objects on the same thread in which they were created.

Apple has provided many ways to work with contexts in multithreaded applications. You’ll read all about different concurrency models in Chapter 9, “Multiple Managed Object Contexts.”

The persistent store container

If you thought there were only four pieces to the Core Data stack, you’re in for a surprise! As of iOS 10, there’s a new class to orchestrate all four Core Data stack classes: the managed model, the store coordinator, the persistent store and the managed context.

The name of this class is NSPersistentContainer and as its name implies, it’s a container that holds everything together. Instead of wasting your time writing boilerplate code to wire up all four stack components together, you can simply initialize an NSPersistentContainer, load its persistent stores, and you’re good to go.

Creating your stack object

Now you know what each component does, it’s time to return to DogWalk and implement your own Core Data stack.

As you know from previous chapters, Xcode creates its Core Data stack in the app delegate. You’re going to do it differently. Instead of mixing app delegate code with Core Data code, you’ll create a separate class to encapsulate the stack.

Go to File ▸ New ▸ File…, select the iOS ▸ Source ▸ Swift File template and click Next. Name the file CoreDataStack and click Create to save the file.

Go to the newly created CoreDataStack.swift. You’ll be creating this file piece-by-piece. Start by replacing the contents of the file with the following:

import Foundation
import CoreData

class CoreDataStack {
  private let modelName: String
  
  init(modelName: String) {
    self.modelName = modelName
  }
  
  private lazy var storeContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: self.modelName)
    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        print("Unresolved error \(error), \(error.userInfo)")
      }
    }
    return container
  }()
}

You start by importing the Foundation and CoreData modules. Next, create a private property to store the modelName. Next, create an initializer to save modelName into private property.

Next, you set up a lazily instantiated NSPersistentContainer, passing the modelName you stored during initialization. The only other thing you need to do is call loadPersistentStores(completionHandler:) on the persistent container (despite the appearance of the completion handler, this method doesn’t run asynchronously by default). Finally, add the following lazily instantiated property below modelName:

lazy var managedContext: NSManagedObjectContext = {
  return self.storeContainer.viewContext
}()

Even though NSPersistentContainer has public accessors for its managed context, the managed model, the store coordinator and the persistent stores (via [NSPersistentStoreDescription]), CoreDataStack works a bit differently.

For instance, the only publicly accessible part of CoreDataStack is the NSManagedObjectContext because of the lazy property you just added. Everything else is marked private. Why is this?

The managed context is the only entry point required to access the rest of the stack. The persistent store coordinator is a public property on the NSManagedObjectContext. Similarly, both the managed object model and the array of persistent stores are public properties on the NSPersistentStoreCoordinator.

Finally, add the following method below the storeContainer property:

func saveContext () {
  guard managedContext.hasChanges else { return }
  
  do {
    try managedContext.save()
  } catch let error as NSError {
    print("Unresolved error \(error), \(error.userInfo)")
  }
}

This is a convenience method to save the stack’s managed object context and handle any resulting errors.

Open ViewController.swift and make the following changes. First, import the Core Data module. Add the following below import UIKit:

import CoreData

Next, add the following property below dateFormatter to hold the Core Data stack:

lazy var coreDataStack = CoreDataStack(modelName: "DogWalk")

Modeling your data

Now your shiny new Core Data stack is securely fastened to the main view controller, it’s time to create your data model.

Head over to your Project Navigator and… Wait a second. There’s no data model file! That’s right. Since I generated this sample application without enabling the option to use Core Data, there’s no .xcdatamodeld file.

No worries. Go to File ▸ New ▸ File…, select the iOS ▸ Core Data ▸ Data Model template and click Next.

Name the file DogWalk.xcdatamodeld and click Create to save the file.

Note: You’ll have problems later on if you don’t name your data model file precisely DogWalk.xcdatamodeld. This is because CoreDataStack.swift expects to find the compiled version at DogWalk.momd.

Open the data model file and create a new entity named Dog. You should be able to do this on your own by now, but in case you forgot how, click the Add Entity button on the bottom left.

Add an attribute named name of type String. Your data model should look like this:

You also want to keep track of the walks for a particular dog. After all, that’s the whole point of the app!

Define another entity and name it Walk. Then add an attribute named date and set its type to Date.

Go back to the Dog entity. You might think you need to add a new attribute of type Array to hold the walks, but there is no array type in Core Data. Instead, the way to do this is to model it as a relationship. Add a new relationship and name it walks.

Set the destination to Walk:

You can think of the destination as the receiving end of a relationship. Every relationship begins as a to-one relationship by default, which means you can only track one walk per dog at the moment. Unless you don’t plan on keeping your dog for very long, you probably want to track more than one walk.

To fix this, with the walks relationship selected, open the Data Model Inspector:

Click on the Type dropdown, select To Many and check Ordered. This means one dog can have many walks and the order of the walks matters, since you’ll be displaying the walks sorted by date.

Select the Walk entity and create an inverse relationship back to Dog. Set the destination as dog and the inverse as walks.

It’s OK to leave this relationship as a to-one relationship. A dog can have many walks, but a walk can only belong to one dog — for the purposes of this app, at least.

The inverse lets the model know how to find its way back, so to speak. Given a walk record, you can follow the relationship to the dog. Thanks to the inverse, the model knows to follow the walks relationship to get back to the walk record.

This is a good time to let you know the data model editor has another view style. This entire time you’ve been looking at the table editor style.

Toggle the segmented control on the bottom-right to switch to the graph editor style:

The graph editor is a great tool to visualize the relationships between your Core Data entities. Here the to-many relationship from Dog to Walk is represented with a double arrow. Walk points back to Dog with a single arrow, indicating a to-one relationship.

Feel free to switch back and forth between the two editor styles. You might find it easier to use the table style to add and remove entities and attributes, and the graph style to see the big picture of your data model.

Adding managed object subclasses

In the previous chapter, you learned how to create custom managed object subclasses for your Core Data entities. It’s more convenient to work this way, so this is what you’ll do for Dog and Walk as well.

Like in the previous chapter, you’re going to generate custom managed object subclasses manually instead of letting Xcode do it for you so you can see what’s going on behind the scenes. Open DogWalk.xcdatamodeld, select the Dog entity and set the Codegen dropdown in the Data Model inspector to Manual/None. Repeat the same process for the Walk entity.

Then, go to Editor ▸ Create NSManagedObject Subclass… and choose the DogWalk model, and then both the Dog and Walk entities. Click Create on the next screen to create the files.

As you saw in Chapter 2, doing this creates two files per entity: one for the Core Data properties you defined in the model editor and one for any future functionality you may add to your managed object subclass.

Dog+CoreDataProperties.swift should look like this:

import Foundation
import CoreData

extension Dog {
  
  @nonobjc public class func fetchRequest()
    -> NSFetchRequest<Dog> {
    return NSFetchRequest<Dog>(entityName: "Dog")
  }
  
  @NSManaged public var name: String?
  @NSManaged public var walks: NSOrderedSet?
}

// MARK: Generated accessors for walks
extension Dog {
  
  @objc(insertObject:inWalksAtIndex:)
  @NSManaged public func insertIntoWalks(_ value: Walk,
                                         at idx: Int)
  
  @objc(removeObjectFromWalksAtIndex:)
  @NSManaged public func removeFromWalks(at idx: Int)
  
  @objc(insertWalks:atIndexes:)
  @NSManaged public func insertIntoWalks(_ values: [Walk],
                                         at indexes: NSIndexSet)
  
  @objc(removeWalksAtIndexes:)
  @NSManaged public func removeFromWalks(at indexes: NSIndexSet)
  
  @objc(replaceObjectInWalksAtIndex:withObject:)
  @NSManaged public func replaceWalks(at idx: Int,
                                      with value: Walk)
  
  @objc(replaceWalksAtIndexes:withWalks:)
  @NSManaged public func replaceWalks(at indexes: NSIndexSet,
                                      with values: [Walk])
  
  @objc(addWalksObject:)
  @NSManaged public func addToWalks(_ value: Walk)
  
  @objc(removeWalksObject:)
  @NSManaged public func removeFromWalks(_ value: Walk)
  
  @objc(addWalks:)
  @NSManaged public func addToWalks(_ values: NSOrderedSet)
  
  @objc(removeWalks:)
  @NSManaged public func removeFromWalks(_ values: NSOrderedSet)
}

extension Dog : Identifiable {

}

Like before, the name attribute is a String optional. But what about the walks relationship? Core Data represents to-many relationships using sets, not arrays. Because you made the walks relationship ordered, you’ve got an NSOrderedSet.

Note: NSSet seems like an odd choice, doesn’t it? Unlike arrays, sets don’t allow accessing their members by index. In fact, there’s no ordering at all! Core Data uses NSSet because a set forces uniqueness among its members. The same object can’t feature more than once in a to-many relationship.

If you need to access individual objects by index, you can check the Ordered checkbox in the visual editor, as you’ve done here. Core Data will then represent the relationship as an NSOrderedSet.

Similarly, Walk+CoreDataProperties.swift should look like this:

import Foundation
import CoreData

extension Walk {
  
  @nonobjc public class func fetchRequest()
    -> NSFetchRequest<Walk> {
    return NSFetchRequest<Walk>(entityName: "Walk")
  }
  
  @NSManaged public var date: Date?
  @NSManaged public var dog: Dog?
}

extension Walk : Identifiable {

}

The inverse relationship back to Dog is simply a property of type Dog. Easy as pie.

Note: Sometimes Xcode will create relationship properties with the generic NSManagedObject type instead of the specific class, especially if you’re making lots of subclasses at the same time. If this happens, just correct the type yourself or generate the specific file again.

A walk down persistence lane

Now your setup is complete; your Core Data stack, your data model and your managed object subclasses. It’s time to convert DogWalk to use Core Data. You’ve done this several times before, so this should be an easy section for you.

Pretend for a moment this application will at some point support tracking multiple dogs. The first step is to track the currently selected dog.

Open ViewController.swift and replace the walks array with the following property. Ignore the errors for now, you’ll fix those in a minute:

var currentDog: Dog?

Next, add the following code to the end of viewDidLoad():

let dogName = "Fido"
let dogFetch: NSFetchRequest<Dog> = Dog.fetchRequest()
dogFetch.predicate = NSPredicate(format: "%K == %@",
                                 #keyPath(Dog.name), dogName)

do {
  let results = try coreDataStack.managedContext.fetch(dogFetch)
  if results.isEmpty {
    // Fido not found, create Fido
    currentDog = Dog(context: coreDataStack.managedContext)
    currentDog?.name = dogName
    coreDataStack.saveContext()
  } else {
    // Fido found, use Fido
    currentDog = results.first
  }
} catch let error as NSError {
  print("Fetch error: \(error) description: \(error.userInfo)")
}

First, you fetch all Dog entities with names of "Fido" from Core Data. You’ll learn more about fancy fetch requests like this in the next chapter.

If the fetch request came back with results, you set the first entity (there should only be one) as the currently selected dog.

If the fetch request comes back with zero results, this probably means it’s the user’s first time opening the app. If this is the case, you insert a new dog, name it “Fido”, and set it as the currently selected dog.

Note: You’ve just implemented what’s often referred to as the Find or Create pattern. The purpose of this pattern is to manipulate an object stored in Core Data without running the risk of adding a duplicate object in the process. In iOS 9, Apple introduced the ability to specify unique constraints on your Core Data entities. With unique constraints, you can specify in your data model which attributes must always be unique on an entity to avoid adding duplicates.

Next, replace the implementation of tableView(_:numberOfRowsInSection:) with the following:

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
  currentDog?.walks?.count ?? 0
}

As you can probably guess, this ties the number of rows in the table view to the number of walks set in the currently selected dog. If there is no currently selected dog, return 0.

Next, replace tableView(_:cellForRowAt:) with the following:

func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
  ) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: "Cell", for: indexPath)

  guard let walk = currentDog?.walks?[indexPath.row] as? Walk,
    let walkDate = walk.date as Date? else {
      return cell
  }

  cell.textLabel?.text = dateFormatter.string(from: walkDate)
  return cell
}

Only two lines of code have changed. Now, you take the date of each walk and display it in the corresponding table view cell.

The add(_:) method still has a reference to the old walks array. Comment it out for now; you’ll re-implement this method in the next step:

@IBAction func add(_ sender: UIBarButtonItem) {
  // walks.append(Date())
  tableView.reloadData()
}

Build and run to make sure you have everything hooked up correctly:

Hooray! If you’ve gotten this far, you’ve just inserted a dog into Core Data and are currently populating the table view with his list of walks. This list doesn’t have any walks at the moment, so the table doesn’t look very exciting.

Tap the plus (+) button, and it understandably does nothing. You haven’t implemented anything underneath this control yet! Before transitioning to Core Data, add(_:) simply added a Date to an array and reloaded the table view. Re-implement it as shown below:

@IBAction func add(_ sender: UIBarButtonItem) {
  // Insert a new Walk entity into Core Data
  let walk = Walk(context: coreDataStack.managedContext)
  walk.date = Date()

  // Insert the new Walk into the Dog's walks set
  if let dog = currentDog,
    let walks = dog.walks?.mutableCopy()
      as? NSMutableOrderedSet {
      walks.add(walk)
      dog.walks = walks
  }

  // Save the managed object context
  coreDataStack.saveContext()

  // Reload table view
  tableView.reloadData()
}

The Core Data version of this method is much more complicated. First, you have to create a new Walk entity and set its date attribute to now. Next, you have to insert this walk into the currently selected dog’s list of walks.

However, the walks attribute is of type NSOrderedSet. NSOrderedSet is immutable, so you first have to create a mutable copy (NSMutableOrderedSet), insert the new walk and then reset an immutable copy of this mutable ordered set back on the dog.

Note: Is adding a new object into a to-many relationship making your head spin? Many people can sympathize, which is why Dog+CoreDataProperties contains generated accessors to the walks ordered set that will handle all of this for you.

For example, you can replace the entire if-let statement in the last code snippet with the following:

currentDog?.addToWalks(walk)

Give it a try!

Core Data can make things easier for you, though. If the relationship weren’t ordered, you’d just be able to set the one side of the relationship (e.g., walk.dog = currentDog) rather than the many side and Core Data would use the inverse relationship defined in the model editor to add the walk to the dog’s set of walks.

Finally, you commit your changes to the persistent store by calling saveContext() on the Core Data stack and you reload the table view.

Build and run the app, and tap the plus (+) button a few times.

Great! The list of walks should now be saved in Core Data. Verify this by terminating the app in the fast app switcher and re-launching from scratch.

Deleting objects from Core Data

Let’s say you were too trigger-friendly and tapped the plus (+) button when you didn’t mean to. You didn’t actually walk your dog, so you want to delete the walk you just added.

You’ve added objects to Core Data, you’ve fetched them, modified them and saved them again. What you haven’t done yet is delete them — but you’re about to do that next.

First, open ViewController.swift and add the following method to the UITableViewDataSource extension:

func tableView(_ tableView: UITableView,
               canEditRowAt indexPath: IndexPath) -> Bool {
  true
}

You’re going to use UITableView’s default behavior for deleting items: swipe left to reveal the red Delete button, then tap on it to delete.

The table view calls this UITableViewDataSource method to ask if a particular cell is editable, and returning true means all the cells should be editable.

Next, add the following method to the same UITableViewDataSource extension:

func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCell.EditingStyle,
  forRowAt indexPath: IndexPath
) {

  //1
  guard let walkToRemove =
    currentDog?.walks?[indexPath.row] as? Walk,
    editingStyle == .delete else {
      return
  }

  //2
  coreDataStack.managedContext.delete(walkToRemove)

  //3
  coreDataStack.saveContext()

  //4
  tableView.deleteRows(at: [indexPath], with: .automatic)
}

This table view data source method is called when you tap the red Delete button. Let’s go through the code step-by-step:

  1. First, you get a reference to the walk you want to delete.
  2. Remove the walk from Core Data by calling NSManagedObjectContext’s delete() method. Core Data also takes care of removing the deleted walk from the current dog’s walks relationship.
  3. No changes are final until you save your managed object context — not even deletions!
  4. Finally, if the save operation succeeds, you animate the table view to tell the user about the deletion.

Build and run the app one more time. You should have several walks from previous runs. Pick any and swipe to the left.

Tap on the Delete button to remove the walk. Verify that the walk is actually gone by terminating the app and re-launching from scratch. The walk you just removed is gone for good. Core Data giveth and Core Data taketh away.

Note: Deleting used to be one of the most “dangerous” Core Data operations. Why is this? When you remove something from Core Data, you have to delete both the record on disk as well as any outstanding references in code.

Trying to access an NSManagedObject that had no Core Data backing store resulted in the the much-feared inaccessible fault Core Data crash.

Starting with iOS 9, deletion is safer than ever. Apple introduced the property shouldDeleteInaccessibleFaults on NSManagedObjectContext, which is turned on by default. This marks bad faults as deleted and treats missing data as NULL/nil/0.

Key points

  • The Core Data stack is made up of five classes: NSManagedObjectModel, NSPersistentStore, NSPersistentStoreCoordinator, NSManagedObjectContext and the NSPersistentContainer that holds everything together.
  • The managed object model represents each object type in your app’s data model, the properties they can have, and the relationship between them.
  • A persistent store can be backed by a SQLite database (the default), XML, a binary file or in-memory store. You can also provide your own backing store with the incremental store API.
  • The persistent store coordinator hides the implementation details of how your persistent stores are configured and presents a simple interface for your managed object context.
  • The managed object context manages the lifecycles of the managed objects it creates or fetches. They are responsible for fetching, editing and deleting managed objects, as well as more powerful features such as validation, faulting and inverse relationship handling.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.