Note: This article talks about the Core Data (via Magical Record) so I’m assuming that you are already familiar with it or at least you know the basics. If this it is not the case you can find very good tutorials on the Tim Roadely’s blog or on the Ray Wenderlich’s blog.
Note 2 (october 2012): The article has been updated to work with the MagicalRecord version 2.
If you have already work with the Core Data framework you probably found this hard to learn. When you start you have to understand what is the role of each components (NSManagedObject, NSManagedObjectContext, NSManagedObjectModel, NSPersistentStoreCoordinator, etc.) and how they work/communicate between them. Once you understand the principle, you notice that the code is not easy to do too. There is a tons of line of code to manage and it becomes a nightmare when you have to work in a multi-threading context. Here comes the very useful open-source project named Magical Record.
This post is intended to give an introduction to using the Magical Record project by showing and explaining you some code snippet extract from a little project which is available here.
Introduction
To define quickly Magical Record, it is a set of classes/categories build over Core Data framework. It provides a lot of convenient methods which make the Core Data programming easier because it is based over the Active Records pattern:
Active record is an approach to accessing data in a database. A database table or view is wrapped into a class; thus an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database; when an object is updated, the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.
The Active Record fetching implemented in this project comes from the Ruby on Rails’ principles in order to provide a clean and light code. Before to show you how works Magical Records we are going to explain you how to “install’ it into your project.
Installation
Before to start, it is important to know how to install the Magical Record into your project. This is very simple! Follow these steps:
- Download the Magical Record from github here: https://github.com/magicalpanda/MagicalRecord
- Add the library into your project by dragging it into Xcode
- Link the CoreData Framework to your project (Project navigator > Targets > Build Phases > Link Binary With Libraries > + > CoreData.framework > Add)
- To make our life easier, add this import into yourprojectname-Prefix.pch:
#import "CoreData+MagicalRecord.h"
(This line makes the Magical Record classes available throughout all the project)
Make sure that the project compile correctly!
So now that you know how to install it, we are going to define a simple model to illustrate the Magical Record features.
Defining Our Model
As part of this article we need to create a model to illustrate the operations provide by the Magical Records project. The model that we are going to build is simple entity named “Person” with an age, firstname and lastname as attributes.
Let’s define the model by following these steps:
- Create a new model named TestModel (File > New File… > Core Data > Data Model) [image shadow=”true” lightbox=”true” size=”small” align=”center” title=”XCode 4.2″ caption=”Create TestModel.xcodedatamodel”]http://yannickloriot.com/wp-content/uploads/2012/03/Create-TestModel.xcodedatamodel.png[/image]
- Add a new Entity named Person (Add Entity)
- Add the attributes age (Integer16), firstname (String) and lastname (String)
- Generate the NSManagedObject (Editor > Create NSManagedObject Subclass… > Create) subclass to allow us to work easily with our entities
We now have our model and we can work with the Core Data framework and more specifically the Magical Record.
Working with Magical Record
In this part we are going to explain you how to work with the Core Data framework by using the Magical Record project.
Initializing and Clearing Core Data
If you have already made projects which using Core Data you must certainly remember that it requires a lot of boilerplate code to work it and especially during the initialization. A little reminder:
// In the AppDelegate.h
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;
// In the AppDelegate.m
- (void)applicationWillTerminate:(UIApplication *)application {
// Saves changes in the application's managed object context before the application terminates.
[self saveContext];
}
- (void)saveContext {
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
#pragma mark - Core Data stack
/**
Returns the managed object context for the application.
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
*/
- (NSManagedObjectContext *)managedObjectContext {
if (__managedObjectContext != nil) {
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
__managedObjectContext = [[NSManagedObjectContext alloc] init];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return __managedObjectContext;
}
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created from the application's model.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (__managedObjectModel != nil) {
return __managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"blaba" withExtension:@"momd"];
__managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return __managedObjectModel;
}
/**
Returns the persistent store coordinator for the application.
If the coordinator doesn't already exist, it is created and the application's store added to it.
*/
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (__persistentStoreCoordinator != nil) {
return __persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"blaba.sqlite"];
NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return __persistentStoreCoordinator;
}
#pragma mark - Application's Documents directory
/**
Returns the URL to the application's Documents directory.
*/
- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
That does not really want to delve into it. However, this code is something simple because it just instantiate Core Data with its model and the data layer.
Now the same code as above using the Magical Record addition:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[MagicalRecord setupCoreDataStackWithStoreNamed:@"MyDatabase.sqlite"];
// ...
return YES;
}
- (void)applicationWillTerminate:(UIApplication *)application {
[MagicalRecord cleanUp];
}
It looks more readable, no?! These 2 lines will hide all the previous boilerplate by creating the NSPersistentStoreCoordinator (linked to the “MyDatabase” sqlite database), the NSManagedObjectModel and the NSManagedObjectContext.
Persisting Entities
Now that we have initialized the Core Data components using Magical Record, we are ready to persist data to the database. From inside a controller (or elsewhere), this is pretty easy:
- (void)persistNewPersonWithFirstname:(NSString *)firstname lastname:(NSString *)lastname age:(NSNumber *)age {
// Get the local context
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
// Create a new Person in the current thread context
Person *person = [Person MR_createInContext:localContext];
person.firstname = firstname;
person.lastname = lastname;
person.age = age;
// Save the modification in the local context
// With MagicalRecords 2.0.8 or newer you should use the MR_saveNestedContexts
[localContext MR_save];
}
As you can see we Magical Record provides a static method for the NSManagedObject which allow us to create a new entity in a very convenient way. In this example, I willingly retrieve the local managed context because if you work in a multi-threaded environment it will avoid inconsistent/concurrent errors. To finish, we don’t forget to save the person into the database!
Fetching Entities
Fetching an entity back out of the database is even easier. We can take advantage of the useful MR_find, MR_findAll and MR_findFirst methods to easily fetch objects based on multiple conditions and sorts:
// Query to find all the persons store into the database
NSArray *persons = [Person MR_findAll];
// Query to find all the persons store into the database order by their 'firstname'
NSArray *personsSorted = [Person MR_findAllSortedBy:@"firstname" ascending:YES];
// Query to find all the persons store into the database which have 25 years old
NSArray *personsWhoHave22 = [Person MR_findByAttribute:@"age" withValue:[NSNumber numberWithInt:25]];
// Query to find the first person store into the databe
Person *person = [Person MR_findFirst];
It exists a lot of other convenient methods to retrieve the data, I invite you to check theAPI.
Updating an Entity
Once you have fetched an object from Core Data, updating it is easy.
- (void)updateAge:(NSNumber *)newAge ofPersonWithFirstname:(NSString *)firstname lastname:(NSString *)lastname {
// Get the local context
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
// Build the predicate to find the person sought
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"firstname ==[c] %@ AND lastname ==[c] %@", firstname, lastname];
Person *personFounded = [Person MR_findFirstWithPredicate:predicate inContext:localContext];
// If a person was founded
if (personFounded) {
// Update its age
personFounded.age = newAge;
// Save the modification in the local context
// With MagicalRecords 2.0.8 or newer you should use the MR_saveNestedContexts
[localContext MR_save];
}
}
Updating an object involves just three steps:
- Fetching data from the Core Data as seen before
- Modifying the object
- Saving the entity on the local object context
Deleting an Entity
Deleting data is very similar, it just requires to call the MR_delete entity method:
- (void)deleteFirstPersonWithFirstname:(NSString *)firstname {
// Get the local context
NSManagedObjectContext *localContext= [NSManagedObjectContext MR_contextForCurrentThread];
// Retrieve the first person who have the given firstname
Person *personFounded = [Person MR_findFirstByAttribute:@"firstname" withValue:firstname inContext:localContext];
if (personFounded) {
// Delete the person found
[personFounded MR_deleteInContext:localContext];
// Save the modification in the local context
// With MagicalRecords 2.0.8 or newer you should use the MR_saveNestedContexts
[localContext MR_save];
}
}
As always, remember to save the context.
Summary
With Magical Record, you can focus on your entities and how they’re useful in your application and do not worry about the Core Data boilerplate code needed. This is because Magical Record hides a lot lower mechanism to developers and it is based over the Active Record Pattern which is a more convenient way (in my opinion) to work with datas.
And even though Magical Record revolves around a simple concept, it’s incredibly powerful, in particular in a multi-threaded environment by providing a high abstraction level to avoid the headache.
Before to finish you can find here an example project which summarizes what has been said in this article.
I hope it will help you. If you have any question, contact me or leave a comment!