- Persisting Objects to Disk
- The Core Data Approach
- Examining the Xcode Core Data Templates
- Summary
The Core Data Approach
Core Data, on the other hand, combines all the speed and efficiency of database storage with the object-oriented goodness of object serialization.
Entities and Managed Objects
When you create your model objects, instead of starting out by writing the .h @interface for the class, you typically begin by modeling your entities, using the Xcode Data Modeler. An entity corresponds to one type of object, such as a Patient or Doctor object, and sets out the attributes for that entity, such as firstName and lastName. You use the data modeler to set which attributes will be persisted to disk, along with various other features such as the type of data that an attribute will hold, data validation requirements, or whether an attribute is optional or required.
When you work with actual instances of model objects, such as a specific Patient object, you're dealing with an instance of a managed object. These objects will either be instances of the NSManagedObject class, or a custom subclass of NSManagedObject. If you don't specify a custom subclass in the modeler, you would typically access the attributes of the object through Key Value Coding (KVC), using code like that in Listing 2.1.
Listing 2.1. Accessing the attributes of a managed object
NSManagedObject *aPatientObject; // Assuming this has already been fetched NSString *firstName = [aPatientObject valueForKey:@"firstName"]; NSString *lastName = [aPatientObject valueForKey:@"lastName"]; [aPatientObject setValue:@"Pain killers" forKey:@"currentMedication"]; [aPatientObject setValue:@"Headache" forKey:@"currentIllness"];
If you choose to do so, you can also provide your own subclass of NSManagedObject, to expose accessor methods and/or properties for your managed object, so you could use the code shown in Listing 2.2. You'll look at this in more detail in Chapter 6, "Working with Managed Objects."
Listing 2.2. Using a custom subclass of NSManagedObject
Patient *aPatientObject; // Assuming this has already been fetched NSString *firstName = [aPatientObject firstName]; NSString *lastName = aPatientObject.lastName; [aPatientObject setCurrentMedication:@"Pain killers"]; aPatientObject.currentIllness = @"Headache";
You could also still access the values of the object using valueForKey:, etc., if you wish.
Relationships
The Data Modeler is also the place where you define the relationships between your entities. As an example, a Patient object would have a relationship to a Doctor, and the Doctor would have a relationship to the Patient, as shown in Figure 2.1.
Figure 2.1 A Patient-Doctor relationship
When modeling relationships, you typically think in relational database terms, such as one-to-one, one-to-many, and many-to-many. In the example shown in Figure 2.1, a patient has only one doctor, but a doctor has many patients, so the doctor-patient relationship is one-to-many.
If the doctor-patient relationship is one-to-many, the inverse relationship (patient-doctor) is obviously many-to-one. When you model these relationships in the Data Modeler, you need to model them both, explicitly, and set one as the inverse of the other. By setting the inverse relationship explicitly, Core Data ensures the integrity of your data is automatically maintained; if you set a patient to have a particular doctor, the patient will also be added to the doctor's list of patients without you having to do it yourself.
You specify a name for each relationship, so that they are exposed in a similar way to an entity's attributes. Again, you can either work with KVC methods, or provide your own accessors and property declarations in a custom subclass, using code like that in Listing 2.3.
Listing 2.3. Working with relationships
Patient *aPatientObject; // Assuming this has already been fetched Doctor *aDoctorObject = [aPatientObject valueForKey:@"doctor"]; Patient *anotherPatientObject; // also already fetched anotherPatientObject.doctor = aDoctorObject; // The inverse relationship is automatically set too NSLog(@"Doctor's patients = %@", [aDoctorObject patients]); /* Outputs: Doctor's patients = ( aPatientObject, anotherPatientObject, etc... ) */
It's important to note that Core Data doesn't maintain any order in collections of objects, including to-many relationships. You'll see later in the book how objects probably won't be returned to you in the order in which you input them. If order is important, you'll need to keep track of it yourself, perhaps using an ascending numerical index property for each object.
If you're used to working with databases such as MySQL, PostgreSQL, or MS SQL Server (maybe with web-based applications in Ruby/Rails, PHP, ASP.NET, etc.), you're probably used to every record in the database having a unique id of some sort. When you work with Core Data, you don't need to model any kind of unique identifier, nor do you have to deal with join tables between related records. Core Data handles this in the background; all you have to do is to define the relationships between objects, and the framework will decide how best to generate the underlying mechanisms, behind the scenes.
Managed Object Contexts
So far, the code in this chapter has assumed that you've fetched an object "from somewhere." When you're working with managed objects and Core Data, you're working within a certain context, known as the Managed Object Context. This context keeps track of the persistent storage of your data on disk (which on iOS is probably a SQLite store) and acts as a kind of container for the objects that you work with.
Conceptually, it's a bit like working with a document object in a desktop application—the document represents the data stored on disk. It loads the data from disk when a document is opened, perhaps allowing you to display the contents in a window on screen. It keeps track of changes to the document, likely holding them in memory, and is then responsible for writing those changes to disk when it's time to save the data.
The Managed Object Context (MOC) works in a similar way. It is responsible for fetching the data from the store when needed, keeping track of the changes made to objects in memory, and then writing those changes back to disk when told to save. Unless you specifically tell the MOC to save, any changes you make to any managed objects in that context will be temporary, and won't affect the underlying data on disk.
Unlike a normal document object, however, you are able to work with more than one managed object context at a time, even though they all relate to the same underlying data. You might, for example, load the same patient object into two different contexts, and make changes to the patient in one of the contexts (as shown in Figure 2.2). The object in the other context would be unaffected by these changes, unless you chose to save the first context. At that point, a notification would be sent to inform you that another context had changed the data, and you could reload the second context if you wanted to.
Figure 2.2 Managed Object Contexts and their Managed Objects
Although it's less common to work with multiple contexts on iOS than it is on the desktop, you typically use a separate context if you're working with objects in the background, such as pulling information from an online source and saving it into your local app's data. If you choose to use the automatic Undo handling offered by managed object contexts, you might set up a second context to work with an individual object, handling undo for any changes to individual attributes as separate actions. When it was time to save that object back into your primary context, the act of saving all those changes would count as one undo action in the primary context, allowing the user to undo all the changes in one go if they wanted to. You'll see examples of this in later chapters.
Fetching Objects
The managed object context is also the medium through which you fetch objects from disk, using NSFetchRequest objects. A fetch request has at minimum the name of an entity; if you wanted to fetch all the patient records from the persistent store, you would create a fetch request object, specify the Patient entity to be retrieved, and tell the MOC to execute that fetch request. The MOC returns the results back to you as an array. Again, it's important to note that the order in that array probably won't be the same as the order in which you stored the objects, or the same as the next time you execute the fetch request, unless you request the results to be sorted in a particular order.
To fetch specific objects, or objects that match certain criteria, you can specify a fetch predicate; to sort the results in a certain order, you can provide an array of sort descriptors. You might choose to fetch all the patient records for a particular doctor, sorting them by last name. Or, if you had previously stored a numerical index on each patient as they were stored, you could ask for the results to be sorted by that index so that they would be returned to you in the same order each time.
Faulting and Uniquing
Core Data also works hard to optimize performance and keep memory usage to a minimum, using a technique called faulting.
Consider what could happen if you loaded a Patient record into memory; in order that you have access to that patient's Doctor object, it might seem that you'd want to have the Doctor object loaded as well. And, since you might need to access the other patients related to that doctor, you should probably load all those Patient objects too. With this behavior, what you thought was a single-object fetch could turn into a fetch of thousands of objects—every related object would need to be fetched, possibly resulting in fetching your entire dataset.
To solve this problem, Core Data doesn't fetch all the relationships on an object. It simply returns you the managed object that you asked for, with the relationships set to faults. If you try and access one of those relationships, such as asking for the name of the patient's doctor, the "fault will fire" and Core Data will fetch the requested object for you. And, as before, the relationships on a newly fetched doctor object will also be set to faults, ready to fire when you need to access any of the related objects. All of this happens automatically, without you needing to worry about it.
A managed object context will also ensure that if an object has already been loaded, it will always return the existing instance in any subsequent fetches. Consider the code in Listing 2.4.
Listing 2.4. Fetching unique objects
Patient *firstPatient; // From one fetch request Doctor *firstPatientsDoctor = firstPatient.doctor; Patient *secondPatient; // From a second fetch request Doctor *secondPatientsDoctor = secondPatient.doctor; /* If the two patients share the same doctor, then the doctor instance returned after each fault fires will be the same instance: */ if( firstPatientsDoctor == secondPatientsDoctor ) { NSLog(@"Patients share a doctor!"); }
This is known as uniquing—you will only ever be given one object instance in any managed object context for, say, a particular Patient.
Persistent Stores and Persistent Store Coordinators
The underlying data is held on disk in a persistent store. On an iOS device, this is usually a SQLite store. You can also choose to use a binary store or even your own custom atomic store type, but these require the entire object graph to be loaded into memory, which can quickly become a problem on a device with limited resources.
You never need to communicate directly with a persistent store, or worry about how it is storing data. Instead, you rely on the relationship between the managed object context and a persistent store coordinator.
The persistent store coordinator acts as a mediator to the managed object contexts; it's also possible to have a coordinator talk to multiple persistent stores on disk, meaning that the coordinator would expose the union of those stores to be accessed by the managed object contexts.
You won't typically need to worry too much about persistent stores and coordinators unless you want to work with multiple stores or define your own store type. In the next section, you'll see the code from the Xcode template project that sets up the persistent store for you. Once this is dealt with, you'll spend most of your time concentrating on the managed objects, held within managed object contexts.