Core Data from Scratch: Managed Objects and Fetch Requests

With everything about Cora Data data models still fresh in your mind, it's time to start working with Core Data. In this article, we meet NSManagedObject, the class you'll interact with most when working with Core Data. You'll learn how to create, read, update, and delete records.

You'll also get to know a few other Core Data classes, such as NSFetchRequest and NSEntityDescription. Let me start by introducing you to NSManagedObject, your new best friend.

1. Managed Objects

Instances of NSManagedObject represent a record in Core Data's backing store. Remember, it doesn't matter what that backing store looks like. However, to revisit the database analogy, an NSManagedObject instance contains the information of a row in a database table.

The reason Core Data uses NSManagedObject instead of NSObject as its base class for modeling records will make more sense a bit later. Before we start working with NSManagedObject, we need to know a few things about this class.

NSEntityDescription

Each NSManagedObject instance is associated with an instance of NSEntityDescription. The entity description includes information about the managed object, such as the entity of the managed object as well its attributes and relationships.

NSManagedObjectContext

A managed object is also linked to an instance of NSManagedObjectContext. The managed object context to which a managed object belongs, monitors the managed object for changes.

2. Creating a Record

With the above in mind, creating a managed object is pretty straightforward. To make sure a managed object is properly configured, it is recommended to use the designated initializer for creating new NSManagedObject instances. Let's see how this works by creating a new person object.

Open the project from the previous article or clone it from GitHub. Because we won't be building a functional application in this article, we'll do most of our work in the application delegate class, TSPAppDelegate. Open TSPAppDelegate.m and update the implementation of application:didFinishLaunchingWithOptions: as shown below.

The first thing we do, is creating an instance of the NSEntityDescription class by invoking entityForName:inManagedObjectContext:. We pass the name of the entity we want to create a managed object for, @"Person", and a NSManagedObjectContext instance.

Why do we need to pass in a NSManagedObjectContext object? We specify the name that we want to create a managed object for, but we also need to tell Core Data where it can find the data model for that entity. Remember that a managed object context is tied to a persistent store coordinator and a persistent store coordinator keeps a reference to a data model. When we pass in a managed object context, Core Data asks its persistent store coordinator for its data model to find the entity we're looking for.

In the second step, we invoke the designated initializer of the NSManagedObject class, initWithEntity:insertIntoManagedObjectContext:. We pass in the entity description and a NSManagedObjectContext instance. Wait? Why do we need to pass in another NSManagedObjectContext instance? Remember what I wrote earlier. A managed object is associated with an entity description and it lives in a managed object context, which is why we tell Core Data which managed object context the new managed object should be linked to.

This isn't too complex, is it? We've now created a new person object. How do we change its attributes or define a relationship? This is done by leveraging key-value coding. To change the first name of the new person object we just created we do the following.

If you're familiar with key-value coding, then this should look very familiar. Because the NSManagedObject class supports key-value coding, we change an attribute by invoking setValue:forKey:. It's that simple.

One downside of this approach is the ease with which you can introduce bugs by misspelling an attribute or relationship name. Also, attribute names are not autocompleted by Xcode like, for example, property names are. We can remedy this problem, but that's something we'll take a look at a bit later in this series.

Before we continue our exploration of NSManagedObject, let's set the age of newPerson to 44.

If you're unfamiliar with key-value coding, then you might be surprised that we passed in an NSNumber literal instead of an integer, like we defined in our data model. The setValue:forKey: method only accepts objects, no primitives. Keep this in mind.

3. Saving a Record

Even though we now have a new person instance, Core Data hasn't saved the person to its backing store yet. The managed object we created currently lives in the managed object context in which it was inserted. To save the person object to the backing store, we need to save the changes of the managed object context by calling save: on it.

The save: method returns a boolean to indicate the result of the save operation and accepts a pointer to an NSerror object, telling us what went wrong if the save operation is unsuccessful. Take a look at the following code block for clarification.

Build and run the application to see if everything works as expected. Did you also run into a crash? What did the console output tell you? Did it look similar to the output below?

Xcode tells us that it expected an NSDate instance for the first attribute, but we passed in an NSString. If you open the Core Data model we created in the previous article, you'll see that the type of the first attribute is indeed Date. Change it to String and run the application one more time.

Another crash? Even though this is a more advanced topic, it's important to understand what's going on.

Data Model Compatibility

The output in Xcode's console should look similar to the output shown below. Note that the error is different from the previous one. Xcode tells us that the model to open the store is incompatible with the one used to create the store. How did this happen?

When we first launched the application a few moments ago, Core Data inspected the data model and, based on that model, created a store for us, a SQLite database in this case. Core Data is clever though. It makes sure that the structure of the backing store and that of the data model are compatible. This is vital to make sure that we get back from the backing store what we expect and what we put there in the first place.

During the first crash, we noticed that our data model contained a mistake and we changed the type of the first attribute from Date to String. In other words, we changed the data model even though Core Data had already created the backing store for us based on the incorrect data model.

After updating the data model, we launched the application again and ran into the second crash. One of the things Core Data does when it creates the Core Data stack is making sure the data model and the backing store—if one exists—are compatible. That was not the case in our example hence the crash.

How do we solve this? The easy solution is to remove the application from the device or from the iOS Simulator, and launch the application again. However, this is something you cannot do if you already have an application in the App Store that people are using. In that case, you make use of migrations, which is something we'll discuss in a future article.

Because we don't have millions of users using our application, we can safely remove the application from our test device and run it once more. If all went well, the new person is now safely stored in the store, the SQLite database Core Data created for us.

Inspecting the Backing Store

You can verify that the save operation worked by taking a look inside the SQLite database. If you ran the application in the iOS Simulator, then navigate to ~/<USER>/Library/Application Support/iPhone Simulator/<VERSION>/<OS>/Applications/<ID>/Documents/Core_Data.sqlite. To make your life easier, I recommend you install SimPholders, a tool that makes navigating to the above path much, much easier. Open the SQLite database and inspect the table named ZPERSON. The table should have one entry, the one we inserted a minute ago.

You should keep two things in mind. First, there's no need to understand the database structure. Core Data manages the backing store for us and we don't need to understand its structure to work with Core Data. Second, never access the store directly. Core Data is in charge of the backing store and we need to respect that if we want Core Data to do its job well. If we start interacting with the SQLite database—or any other store—there is no guarantee Core Data will continue to function properly. In short, Core Data is in charge of the store so leave it alone.

4. Fetching Records

Even though we'll take a close look at NSFetchRequest in the next article, we need the NSFetchRequest class to ask Core Data for information from the object graph it manages. Let's see how we can fetch the record we inserted earlier using NSFetchRequest.

After initializing the fetch request, we create an NSEntityDescription object and assign it to the entity property of the fetch request. As you can see, we use the NSEntityDescription class to tell Core Data what entity we're interested in.

Fetching data is handled by the NSManagedObjectContext class, we invoke executeFetchRequest:error:, passing in the fetch request and a pointer to an NSError object. The method returns an array of results if the fetch request is successful and nil if a problem is encountered. Note that Core Data always returns an NSArray object if the fetch request is successful, even if we expect one result or if Core Data didn't find any matching records.

Run the application and inspect the output in Xcode's console. Below you can see what was returned, an array with one object of type NSManagedObject. The entity of the object is Person.

To access the attributes of the record, we make use of key-value coding like we did earlier. It's important to become familiar with key-value coding if you plan to work with Core Data.

You may be wondering why I log the person object before and after logging the person's name. This is actually one of the most important lessons of this article. Take look at the output below.

The first time we log the person object to the console, we see data: <fault>. The second time, however, data contains the contents of the object's attributes and relationships. Why is that? This has everything to do with faulting, a key concept of Core Data.

5. Faulting

The concept that underlies faulting isn't unique to Core Data. If you've ever worked with Active Record in Ruby on Rails, then the following will certainly ring a bell. The concept isn't identical, but the similar from a developer's perspective.

Core Data tries to keep its memory footprint as low as possible and one of the strategies it uses to accomplish this is faulting. When we fetched the records for the Person entity a moment ago, Core Data executed the fetch request, but it didn't fully initialize the managed objects representing the fetched records.

What we got back is a fault, a placeholder object representing the record. The object is of type NSManagedObject and we can treat it as such. By not fully initializing the record, Core Data keeps its memory footprint low. It's not a significant memory saving in our example, but imagine what would happen if we fetched dozens, hundreds, or even thousands of records.

Faults are generally nothing that you need to worry about. The moment you access an attribute or relationship of a managed object, the fault is fired, which means that Core Data changes the fault into a realized managed object. You can see this in our example and that's also the reason why the second log statement of the person object doesn't print a fault to the console.

Faulting is something that trips up many newcomers and I therefore want to make sure you understand the basics of this concept. We'll learn more about faulting later in this series.

6. Updating Records

Updating records is just as simple as creating a new record. You fetch the record, change an attribute or relationship, and save the managed object context. The idea is the same as when you create a record. Because the managed object, the record, is linked to a managed object context, the latter is aware of any changes, insertions and updates. When the managed object context is saved, everything is propagated to the backing store by Core Data.

Take a look at the following code block in which we update the record we fetched by changing the person's age and saving the changes.

You can verify that the update was successful by taking another look at the SQLite store as we did earlier.

7. Deleting Records

Deleting a record follows the same pattern as creating and updating records. We tell the managed object context that a record needs to be deleted from the persistent store by invoking deleteObject: and passing the managed object that needs to be deleted.

In our project, delete the person object we fetched earlier by passing it to the managed object context's deleteObject: method. Note that the delete operation isn't committed to the backing store until we call save: on the managed object context.

Conclusion

In this tutorial, we've covered a lot more than just creating, fetching, updating, and deleting records. We've touched on a few important concepts on which Core Data relies, such as faulting and data model compatibility.

In the next installment of this series, you'll learn how to create and update relationships, and we take an in-depth look at the NSFetchRequest class. We'll also start using NSPredicate and NSSortDescriptor to make our fetch requests flexible, dynamic, and powerful.

Tags:

Comments

Related Articles