Advertisement

Core Data from Scratch: Managed Objects and Fetch Requests

by
This post is part of a series called Core Data from Scratch.
Core Data from Scratch: Data Model
Core Data from Scratch: Relationships and More Fetching

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.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    // Configure Window
    [self.window setBackgroundColor:[UIColor whiteColor]];
    [self.window makeKeyAndVisible];
    
    // Create Managed Object
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
    NSManagedObject *newPerson = [[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext];
    
    return YES;
}

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.

[newPerson setValue:@"Bart" forKey:@"first"];
[newPerson setValue:@"Jacobs" forKey:@"last"];

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.

[newPerson setValue:@44 forKey:@"age"];

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.

NSError *error = nil;

if (![newPerson.managedObjectContext save:&error]) {
    NSLog(@"Unable to save managed object context.");
    NSLog(@"%@, %@", error, error.localizedDescription);
}

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?

Core Data[1218:38496] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for attribute: property = "first"; desired type = NSDate; given type = __NSCFConstantString; value = Bart.'
*** First throw call stack:
(
  0   CoreFoundation                      0x01f4d646 __exceptionPreprocess + 182
  1   libobjc.A.dylib                     0x01bef8e3 objc_exception_throw + 44
  2   CoreData                            0x00308e6e _PFManagedObject_coerceValueForKeyWithDescription + 3454
  3   CoreData                            0x002db39d _sharedIMPL_setvfk_core + 205
  4   CoreData                            0x00308096 -[NSManagedObject(_PFDynamicAccessorsAndPropertySupport) _setGenericValue:forKey:withIndex:flags:] + 54
  5   CoreData                            0x002f735c _PF_Handler_Public_SetProperty + 108
  6   CoreData                            0x002f72c5 -[NSManagedObject setValue:forKey:] + 181
  7   Core Data                           0x00002beb -[TSPAppDelegate application:didFinishLaunchingWithOptions:] + 891
  8   UIKit                               0x0066bb37 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 291
  9   UIKit                               0x0066c875 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2920
  10  UIKit                               0x0066fa33 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1507
  11  UIKit                               0x00687eb8 __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke + 59
  12  UIKit                               0x0066e77e -[UIApplication workspaceDidEndTransaction:] + 29
  13  FrontBoardServices                  0x04264f1f -[FBSWorkspace clientEndTransaction:] + 87
  14  FrontBoardServices                  0x0426c4ed __53-[FBSWorkspaceClient _queue_handleTransactionBookEnd]_block_invoke + 49
  15  CoreFoundation                      0x01e71f90 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 16
  16  CoreFoundation                      0x01e67133 __CFRunLoopDoBlocks + 195
  17  CoreFoundation                      0x01e66898 __CFRunLoopRun + 936
  18  CoreFoundation                      0x01e6622b CFRunLoopRunSpecific + 443
  19  CoreFoundation                      0x01e6605b CFRunLoopRunInMode + 123
  20  UIKit                               0x0066e095 -[UIApplication _run] + 571
  21  UIKit                               0x006716e5 UIApplicationMain + 1526
  22  Core Data                           0x0000394d main + 141
  23  libdyld.dylib                       0x0250bac9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

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?

Unresolved error Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)" UserInfo=0xcb17a30 {metadata={
    NSPersistenceFrameworkVersion = 508;
    NSStoreModelVersionHashes =     {
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
        Person = <68eb2a17 12dfaf41 510772c0 66d91b3d 7cdef207 4948ac15 f9ae22cc fe3d32f2>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers =     (
        ""
    );
    NSStoreType = SQLite;
    NSStoreUUID = "EBB4C708-F933-4E74-8EE0-47F9972EE523";
    "_NSAutoVacuumLevel" = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata =     {
        NSPersistenceFrameworkVersion = 508;
        NSStoreModelVersionHashes =         {
            Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
            Person = <68eb2a17 12dfaf41 510772c0 66d91b3d 7cdef207 4948ac15 f9ae22cc fe3d32f2>;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
            ""
        );
        NSStoreType = SQLite;
        NSStoreUUID = "EBB4C708-F933-4E74-8EE0-47F9972EE523";
        "_NSAutoVacuumLevel" = 2;
    };
    reason = "The model used to open the store is incompatible with the one used to create the store";
}

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.

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];

NSError *error = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];

if (error) {
    NSLog(@"Unable to execute fetch request.");
    NSLog(@"%@, %@", error, error.localizedDescription);
    
} else {
    NSLog(@"%@", result);
}

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.

Core Data[1588:613] (
    "<NSManagedObject: 0x1094352d0> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: <fault>)"
)

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.

if (result.count > 0) {
    NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];
    NSLog(@"1 - %@", person);
    
    NSLog(@"%@ %@", [person valueForKey:@"first"], [person valueForKey:@"last"]);
    
    NSLog(@"2 - %@", person);
}

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.

Core Data[1659:613] 1 - <NSManagedObject: 0x109382980> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: <fault>)
Core Data[1659:613] Bart Jacobs
Core Data[1659:613] 2 - <NSManagedObject: 0x109382980> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: {
    addresses = "<relationship fault: 0x109380b20 'addresses'>";
    age = 44;
    first = Bart;
    last = Jacobs;
})

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.

NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];

[person setValue:@30 forKey:@"age"];

NSError *saveError = nil;

if (![person.managedObjectContext save:&saveError]) {
    NSLog(@"Unable to save managed object context.");
    NSLog(@"%@, %@", saveError, saveError.localizedDescription);
}

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.

NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];

[self.managedObjectContext deleteObject:person];

NSError *deleteError = nil;

if (![person.managedObjectContext save:&deleteError]) {
    NSLog(@"Unable to save managed object context.");
    NSLog(@"%@, %@", deleteError, deleteError.localizedDescription);
}

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.

Advertisement