Magical Record: how to make programming with Core Data pleasant

Magical Record: how to make programming with Core Data pleasant

  • March 2012
  • Posted By Yannick Loriot
  • 27 Comments

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.

wikipedia

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:

  1. Download the Magical Record from github here: https://github.com/magicalpanda/MagicalRecord
  2. Add the library into your project by dragging it into Xcode

  3. Link the CoreData Framework to your project (Project navigator > Targets > Build Phases > Link Binary With Libraries > + > CoreData.framework > Add)

  4. 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:

  1. 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]
  2. Add a new Entity named Person (Add Entity)
  3. Add the attributes age (Integer16), firstname (String) and lastname (String)

  4. 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 boilderplate 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 the API.

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:

  1. Fetching data from the Core Data as seen before
  2. Modifying the object
  3. 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 developpers 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!

References

For further study or clarify some points you can follow these links:

1 Star2 Stars3 Stars4 Stars5 Stars (28 votes, average: 4.61 out of 5)
Loading...Loading...

Comments

Thanks for the note about "localContext", couldn't wrap my head around this
re: "Add the library into your project by dragging it into Xcode" you should say how to do this. eg: In XCode 4 With the library project closed you drag the project from Finder into your project. ow whatever specific instructions should be used for this project. I found this to be difficult the first few libraries I tried to use.
    Maybe you should be reading articles about the basics of Xcode instead.
Hi, I am new to Obj-C and start learning a few months ago. My question is whether MR can add/remove fields(columns) in an entity or not. Thanks
Nice example. But try to add new version on you model :-) TestModel.xcdatamodeld i aded 3 and after last one i get [MagicalRecordHelpers defaultErrorHandler:](0x20cc4) Error: { NSPersistenceFrameworkVersion = 386; NSStoreModelVersionHashes = { Person = ; }; NSStoreModelVersionHashesVersion = 3; NSStoreModelVersionIdentifiers = ( "" ); NSStoreType = SQLite; NSStoreUUID = "190AD4BF-BA1E-466B-9297-C87DB6732A04"; "_NSAutoVacuumLevel" = 2; } Som what to do with this type of error?
    Andrew - any luck with versioning of of your model? I'm stuck with same error, despite the magical manual saying the problem of versioning should be gone... thanks!
      the answer is... ;) to change the data stack init method in AppDelegate to [MagicalRecord setupAutoMigratingCoreDataStack];
hi, i user magical record in my current application. i follow instruction which You mention, but my database.sqlite is not get copied in document directory. please help me , i am stuck here. Thanks,
    hi, i got my answer mydatabase.sqlite file is stored in library instead of document directory. thanks,
hi, Thanks for sharing this post. I have a little problem with relationship. I use a tableviewcontroller with a NSFetchedResultsController self.clientFRC = [Client MR_fetchAllSortedBy:@"companyName" ascending:YES withPredicate:nil groupBy:nil delegate:self inContext:[NSManagedObjectContext MR_contextForCurrentThread]]; My entity Client has a relationship with another one called Country. But when I try to display the country name from one item, it comes with a null value. Is there some thing to add to fetch the relationship? Thanks
Thanks alot for your work! This is the best thing i haver seen since the invention of BASIC! Why they make it so hard, who wants to work with data in such a low level? We cant make really good stuff if we keep concentrated so much on syntax and avoidable things. We really hit it on the heart! Keep doing that good stuff ;)
I just downloaded MagicalRecord from GitHub, installed it following your instructions and everything was going well until I added the code for AppDelegate.m. There is no MagicalRecordHelpers in the version from GitHub... I did find it on the web in your "MagicalRecordTestProject", but not in "MagicalPanda's GitHub. I really don't understand why they are different... How do I resolve this?
    Hi, This article is not up to date yet because since I wrote it the version 2 has been released (I'll updated it soon). So that's why you don't find the MagicalRecordHelpers into the git repo. Now the MagicalRecordHelpers and the MRCoreDataAction have been replaced with a single class named MagicalRecord. So now to setup your Core Data, you just need to replace the MagicalRecordHelpers by this: [MagicalRecord setupCoreDataStackWithStoreNamed:@"MyDatabase.sqlite"];
      Thank you... kinda figured that was the correct way, but being a noob I needed confirmation.
I understand that it must be really hard to keep the articles updated... but just to save 3 hours of the next reader's life, since MR 2.0.8, the method save doesn't save it to the database... just to the current context, then... on your code, you should change [localContext MR_save]; with [localContext MR_saveNestedContexts]; so that it goes up to the root context and actually persists the changes into the sqlite database. just my two cents ;) thanks for a great article about a great library
    Yes that's a bit hard to keep the articles up to date and sometimes (like here) I add some errors while trying to do this task. Thank you for your help, I change that now!
    Just keep in mind that MR_saveNestedContexts is async and returns immediately while MR_save waits for the save to be complete before returning. This change is actually causing some issues since it's not that easy anymore to do a nested sync save.
      Wow, that's interesting, and definitely not expected... maybe we should use MR_saveNestedContextsErrorHandler:completion: ? just checked the source code at NSManagedObjectContext+MagicalSaves.m and it seems that just sending the completion block would do the trick
Hi guys, Has anyone noticed magical record being unreliable on saves? I am using the MR_saveNestedContexts command on my context, and no matter what method I use, or what kind of context, it usually persists nothing, and when I use a block, the completion handler fires, not the error handler, but still nothing is persisted! One of my objects is persisting, but never on the first run of the application, it fails to persist, then if I stop the app and start again it persists. What is going on? Could it be some other database code is interfering with the operation of magical record? I have a core data stack, and have other data access code in my app that I am attempting to phase out, could this be a problem? I am truly at a loss as to why saves seem to work at times, but mostly do not work, no matter what method I try, and there doesn't seem to be any rhyme or reason to it. Any info would be greatly appreciated...
    Just an update, I finally realized that magical record was indeed saving, but it was saving to a different database! It creates the database exactly the same way that core data would but the database is created in a different location by default. Normally this would not be an issue, but when you're transitioning from your own code to magical record, this could cause a great deal of confusion. If you're going to use magical record, it's a good idea to go ahead and remove all other database code and use it exclusively.
Nice little tutorial. Thanks much!
Really appreciate your tutorial. Very clear. The amount of boiler code for Core data was putting me off. But having seen how magic record reduces it so radically, I think I will give it a go. Thanks again for tutorial.
Nice tutorial. How about multiple threads? How to deal with background threads saving to the database while the UI is updated on the main thread?
Just one thing, from the documentation: /// \discussion Replaced by \MR_saveToPersistentStoreAndWait So it's not MR_saveNestedContexts. Great article
Any idea how to fetch records in a way that some with null values in sorting columns, come as last?
A great tutorial. One thing as an aside, I really wish the authors of this framework would correct their usage of the word "Entity" and not use it interchangeably with "ManagedObject". "Person" is an Entity, but when you create a Person, you are creating an object, not an entity. Similarly, at run time you would not update "entities" but rather objects. One would not randomly throw around the word "Class" the way they are throwing around "Entity". If I have a Car class, myCar is not a Class, it is an object of type Car.
Anyone tried implementing this into a project that already had coredata in it? I've got a new project I've been bashing my head against a wall with - it's already started with CoreData. How hard would it be to get rid of my existing coredata code and implement this? Excited to try because I feel it would perfectly fit my needs.

Leave a Reply

Your email address will not be published. Required fields are marked *


four − three =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>