Home » Core Data

Core Data: Importing Data

5 May 2009 5,180 views 9 Comments

Importing data from a foreign source, whether it be from a server, another file generated on the same operating system or a file from another operating system, is a fairly common action for an application to perform. We are going to walk through one solution for importing data from a CSV file into a Core Data store that makes use of the NSOperation API and therefore multi-threading. In addition, thanks to Simon Wolf, we will be using an elegant method for notifying the main thread that the data is being imported so that it can refresh the UI.

Setting up the application

The first step in this exercise is to create our test project. We will start off with a single persistent store Core Data application.

New Project Window

With the new project created we need to update the AppDelegate’s header to handle all of the actions we are going to be performing. Since we are going to be doing the import on a background thread we want a way to disable certain parts of the UI during the import. We could grab bindings for all of the UI elements and walk through them as needed but there is a cleaner solution.

@interface AppDelegate : NSObject
{
  NSWindow *window;
 
  BOOL importing;
 
  NSPersistentStoreCoordinator *persistentStoreCoordinator;
  NSManagedObjectModel *managedObjectModel;
  NSManagedObjectContext *managedObjectContext;
 
  NSString *filePath;
 
  NSOperationQueue *queue;
}
 
@property (assign) IBOutlet NSWindow *window;
@property (assign, getter=isImporting) BOOL importing;
@property (retain) NSString *filePath;
 
- (NSPersistentStoreCoordinator*)persistentStoreCoordinator;
- (NSManagedObjectModel*)managedObjectModel;
- (NSManagedObjectContext*)managedObjectContext;
 
- (IBAction)saveAction:(id)sender;
- (IBAction)chooseFile:(id)sender;
- (IBAction)import:(id)sender;
 
- (void)mergeChanges:(NSNotification*)notification;
 
@end

In our AppDelegate’s header we have a single BOOL variable that will notify any listeners that the import is in progress. This way the UI can manage itself with regard to enabling and disabling elements. This also gives us a nice clean separation from the UI.

In addition to the BOOL we have added a NSString for the filePath, this will retain a reference to the file we are going to be importing and a NSOperationQueue which we will keep for the life of our application.

With regard to methods, we have added three methods to handle the import operation.

-chooseFile:

The -chooseFile: implementation will present the user with a NSOpenFile dialog which will allow the user to select a csv file. The implementation is as follows

- (IBAction)chooseFile:(id)sender;
{
  NSOpenPanel *openPanel = [NSOpenPanel openPanel];
  [openPanel setCanChooseDirectories:NO];
  [openPanel setCanCreateDirectories:NO];
  [openPanel beginSheetForDirectory:nil
    file:nil
    types:[NSArray arrayWithObject:@"csv"]
    modalForWindow:window
    modalDelegate:self
    didEndSelector:@selector(fileOpenDidEnd:returnCode:context:)
    contextInfo:nil];
}

Since this is a modal sheet we need a callback once the user has selected the file they want to import. In this case the callback method is -fileOpenDidEnd:returnCode:context: and is implemented as follows:

- (void)fileOpenDidEnd:(NSOpenPanel*)openPanel
            returnCode:(NSInteger)code
            context:(void*)context
{
  if (code == NSCancelButton) return;
  [self setFilePath:[openPanel filename]];
}

We first check to see if the user hit the cancel button (or escape) and if they did not then we set our filePath property to the value of the -filename method of the passed in NSOpenPanel. If our UI is watching the filePath property then it would automatically update itself at this point.

-import:

Once the user has selected the file that they want to import the next step is to kick off the import. We are going to be doing the import in a subclass of NSOperation so we want to pass off a reference to the AppDelegate. We pass in the AppDelegate for a few reasons. We want to be notified when the import is complete and we want the main NSManagedObjectContext to be alerted when data has been changed within the NSOperation. Lastly, we store the location of the file to be imported in the AppDelegate. With this in mind we need to implement the -import: method as follows:

- (IBAction)import:(id)sender;
{
  ImportOperation *operation = nil;
  operation = [[ImportOperation alloc] initWithDelegate:self];
  if (!queue) {
    queue = [[NSOperationQueue alloc] init];
  }
  [queue addOperation:operation];
  [self setImporting:YES];
  [operation release], operation = nil;
}

In this method we initialize the NSOperation subclass and pass in the values it needs to perform its work. We then check to see if our queue property has been initialized and if not we initialize it. Finally we add the NSOperation to the NSOperationQueue and set our importing property to YES. If the UI is watching the importing property it will enable and disable UI elements as needed.

-importDone

Once the NSOperation has completed the import we want it to call back to the AppDelegate so that we can flip the importing property back to NO. This is handled in the -importDone method.

- (void)importDone
{
  [self setImporting:NO];
}

-mergeChanges:

The last new method that we are adding to the AppDelegate is the -mergeChanges: method which handles any changes. As was shown to me by Simon Wolf, Apple added a new method in 10.5 called -mergeChangesFromContextDidSaveNotification: which allows us to process a save notification sent on a different thread and thereby updating the context on the main thread of the application. As you may recall, NSManagedObject and NSManagedObjectContext are not thread safe and should be kept within their own threads at all times (or within complicated locks).

This addition to the NSManagedObjectContext now makes our cross thread notifications trivial!

- (void)mergeChanges:(NSNotification*)notification
{
  NSAssert([NSThread mainThread], @"Not on the main thread");
  [[self managedObjectContext]
        mergeChangesFromContextDidSaveNotification:notification];
}

Because I am thread-paranoid, I have an NSAssert in the method to make sure we are on the main thread before performing the update. Once we are safely past the NSAssert we can then hand off the passed in NSNotification to the NSManagedObjectContext and have it update itself. Since our UI is properly watching our NSManagedObjectContext for changes, it will also update itself as appropriate.

Building the Data Model

The data model for this example will consist of two entities; a Person and an Address.

Data Model

The person entity has the following attributes:

address   Relationship -> Address Entity
age       Integer 16 Optional
firstName String Optional
lastName  String Optional

The address entity has the following attributes:

city      String Optional
person    Relationship -> Person Entity
postal    String Optional
street    String Optional

With this data model we can build person entities that have a single address entity.

Building the User Interface

The user interface for this example will present the user with a way to select a file and display the results of the import.

User Interface

We also have a NSProgressIndicator which we are using as a busy notification to the user. This way when the import is taking longer than a couple of seconds the user knows that the application is actually doing something.

To make all of this work, we need to set up the bindings from our UI to our AppDelegate. To do this we need to add one more elements to the xib and that is a NSArrayController. The NSArrayController will watch the NSManagedObjectContext for changes and will contain an array of Person entities. Therefore when a new person is added to the NSManagedObjectContext it will automatically show up in the NSArrayController which will in turn cause it to appear in the NSTableView. ALl of this is done using KVO and KVC and does not involve any code from us.

Interface Builder

NSArrayController

To properly configure the NSArrayController we need to set its Object Controller to “Entity” mode and set the entity name to “Person”. We also want to have the NSArrayController prepare its content, auto arrange its content.

NSArrayController1

We also need to bind it to a NSManagedObjectContext so that it can retrieve the objects and monitor the context for new objects. Since there is already a reference to the AppDelegate we can bind directly to it.

NSArrayController2

Binding The Main Window

Once the NSArrayController has been set up, we can finish the main window. Within the NSTableView we want to bind the first column’s value to the NSArrayController using a controller key of arrangedObjects and a model key path of firstName. This will tell the column to grab the firstName property from each object in the NSArrayController. The second column should bind to the model key path of lastName using the same controller key. The third and final column should be bound to the model key path of address.city which will cross the relationship to the Person entity’s Address and retrieve the city.

The two buttons should be bound to the AppDelegate using the -import: and -chooseFile: methods. In addition we should bind the enabled property to the AppDelegate so that when the importing property changes, the enabled state of the buttons will change automatically.

Button Enabled Binding

The label (which is really a non-editable NSTextField) should have its value bound to the AppDelegate as well using the filePath property. This will automatically update when the user selects a file to import.

The final UI element to bind is the NSProgressIndicator. Ideally it should be hidden when a import is not occurring and should animate when a import is occurring. To accomplish this we bind its hide property to the importing property of the AppDelegate but we add a value transformer to reverse the boolean value. We do this because we want the NSProgressIndicator to hide when the importing property is set to NO.

Progress Hide Binding

In addition, we want the NSProgressIndicator to animate when an import is in progress. We accomplish this by binding the animate parameter to the importing property of the AppDelegate.

Progress Animate Parameter

In this setting we do not need to reverse the boolean.

Implementing the NSOperation

The last piece we need to implement is the NSOperation subclass. Most of the work for this class is handled in its -main method but we do need to a little set up in the -initWithFile:delegate method. Therefore we also need to implement one property to store the reference to the AppDelegate. We do not need to retain it as we know it will remain in memory for the life of the operation.

@class AppDelegate;
 
@interface ImportOperation : NSOperation
{
  AppDelegate *delegate;
}
 
@property (assign) AppDelegate *delegate;
 
- (id)initWithDelegate:(AppDelegate*)delegate;
 
@end

-initWithDelegate:

The -initWithDelegate: method does exactly what it sounds like it is doing:

- (id)initWithDelegate:(AppDelegate*)aDelegate;
{
  if (!(self = [super init])) return nil;
 
  [self setDelegate:aDelegate];
 
  return self;
}

We assign the passed in reference to the delegate property and that is it. We are ready for the NSOperation to begin.

-main

The -main method is where we are doing all of the work for the import. To begin with, we need to build up our environment.

- (void)main
{
  NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
  [moc setPersistentStoreCoordinator:[[self delegate] persistentStoreCoordinator]];
  [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(contextDidSave:)
      name:NSManagedObjectContextDidSaveNotification
      object:moc];
 
  NSString *data =
          [[NSString alloc] initWithContentsOfFile:[[self delegate] filePath]];
  NSScanner *lineScanner = [NSScanner scannerWithString:data];
  NSString *line = nil;

Since our NSOperation is going to be running on its own thread we need to initialize our own NSManagedObjectContext. However, since the NSPersistentStoreCoordinator is thread safe we can (and should) use the same one within our NSOperation. Therefore we initialize the NSManagedObjectContext and then set its NSPersistentStoreCoordaintor to the same one we are using in the AppDelegate.

Once our NSManagedObjectContext has been initialized we want to observe it for changes. To do this we add ourselves as an observer to that specific instance of NSManagedObjectContext and watch for NSManagedObjectContextDidSaveNotification messages. Whenever we call save on this context a save notification will be sent out which we can then use to update the main thread.

We next need to load in the data to be imported. If this was to be a truly large operation I would not recommend pulling the entire file into memory. However for demonstration purposes we load the data into a NSString. We then initialize a NSScanner against the data and we are ready to start processing.

while ([lineScanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
                                   intoString:&line]) {
  NSArray *elements = [line componentsSeparatedByString:@","];
  NSManagedObject *person =
             [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                  inManagedObjectContext:moc];
  [person setValue:[elements objectAtIndex:0] forKey:@"firstName"];
  [person setValue:[elements objectAtIndex:1] forKey:@"lastName"];
  NSNumber *number =
        [NSNumber numberWithInt:[[elements objectAtIndex:2] intValue]];
  [person setValue:number forKey:@"age"];
 
  NSManagedObject *address =
             [NSEntityDescription insertNewObjectForEntityForName:@"Address"
                                  inManagedObjectContext:moc];
  [address setValue:[elements objectAtIndex:3] forKey:@"street"];
  [address setValue:[elements objectAtIndex:4] forKey:@"city"];
  [address setValue:[elements objectAtIndex:5] forKey:@"postal"];
  [person setValue:address forKey:@"address"];
 
  NSError *error = nil;
 
  if (![moc save:&error]) {
    [NSApp presentError:error];
  }
 
  //Simulated delay for effect
  [NSThread sleepForTimeInterval:1.0];
}

We are going to use the NSScanner to separate out the rows of data. We do this in a while loop and instruct the NSScanner to break the NSString apart using the newlineCharacterSet as the deliminator. This will split the file, pushing one row at a time into the line variable based on all of known line breaks.

With each line we then call -componentsSeparatedByString: using a comma as the string to split the row with. This will break the row up into a NSArray. For a more robust solution we should be looking for quotation marks, etc.

With the data now split into pieces we construct a new person NSManagedObject from our context and start setting the values from the NSArray. When the person entity is finished we construct a new address NSManagedObject and set its values.

Because we want the UI to update after each row, we then call save on the NSManagedObjectContext to persist the new data to disk. In a production system it is not recommended to save this often as writing to disk is a fairly expensive operation.

The last line of code simply slows down the import for demonstration purposes.

Once the entire file has been imported we need to tear down all of our variables so that we can exit cleanly.

[[NSNotificationCenter defaultCenter] removeObserver:self
       name:NSManagedObjectContextDidSaveNotification
       object:moc];
[moc release], moc = nil;
[data release], data = nil;
[[self delegate] importDone];
}

We first remove ourselves as an observer of our NSManagedObjectContext and then we release the context and the data. This insures that we do not have any memory leaks once the NSOperation is complete. Once everything has been torn down properly we notify the AppDelegate that the import is complete so that the UI will reset to its normal state.

-contextDidSave:

The last thing we need to implemenet in our application is the callback method from the NSManagedObjectContext save. in the -contextDidSave: method we call -performSelectorOnMainThread: withObject: waitUntilDone: on the AppDelegate and pass in the notification that we have received. This will call our -mergeChanges: method on the AppDelegate on the main thread with all of the information needed to update the main NSManagedObjectContext which will in turn update the UI.

Conclusion

This example shows quite a few techniques which span multi-threading, importing data, and UI bindings. While there is certainly more than one solution to any problem, this demonstrates several strategies to application development and how to minimize the amount of code that is required. Less code means less bugs and less headaches to maintain in the future.

Example code for this article can be downloaded here

Marcus Zarra

marcusMarcus S. Zarra is the owner of Zarra Studios LLC and the creator of seSales and iWeb Buddy as well as being a co-author of “Cocoa Is My Girlfriend”, a wildly popular blog covering all aspects of Cocoa development. Marcus has been developing software since the mid-1980s and has written software in all of the major technological fields. Marcus has been using Core Data since its original release in OS X 10.4 Tiger and has released numerous applications and papers covering all of the topics of Core Data.

9 Comments »

  • Erik said:

    One of the best tutorials published to this site so far – clean code, interesting topic, and best of all, well-written. It’s obvious that some time was spent writing this, I like how it dipped into a little bit of several different things (NSOperation, Core Data, setting up bindings, etc). Thanks, Zarra.

  • Gary said:

    Thanks Marcus, great article.

  • Stefan said:

    Hi Marcus,
    Thanks for the article, really well written – I picked up a couple of good ideas from it!
    Cheers,
    Stefan

  • Bala Paranj said:

    Excellent article. Where does the data get saved? I need to use it as a starting point for another project that uses Core Data. How can I import the saved Core Data into my new project?

  • Satyam said:

    Bala, All the data will be stored in xml format by default. If you see your code, in the class “….AppDelegate.m” there will be a method “- (NSPersistentStoreCoordinator *) persistentStoreCoordinator “. You can see the name of “.xml file. It will be saved in the folder “/Users//Library/Application Support//.xml”

    Default format of data will be xml. You can also configure it to work with SQLLite.
    If you want to use SQLLite, change the following in above said method:

    url = [NSURL fileURLWithPath: [applicationSupportFolder
    stringByAppendingPathComponent: @".demodoc"]];

    and

    if ([coordinator addPersistentStoreWithType: NSSQLiteStoreType
    configuration: nil
    URL: url
    options: nil
    error: &error])

    Similarly you can use binary store also.
    (Didn’t use any html tags, so might need some formatting)

  • Michele Longhi said:

    Hi marcus

    I just applied what I learned from your great article in a core data document-based application.
    If the document (NSPersistentDocument) has been saved prior to call the import thread, everything work flawlessly. The problem arises with new documents: no stores are present into the coordinator, therefore calling the save method inside the working thread throws an exception. I tried to add an in-memory store to the NSPersistentStoreCoordinato and the changes are merged into the NSManagedObjectContext of the main thread, but nothing is saved with the document.
    Any hint?

  • Michele Longhi said:

    Please forgive me: I wrote “marcus” instead of “Marcus”.
    Best regards – Michele

  • Bala Paranj said:

    That does not answer my question: How can do a one-time import of that data?

  • Hwee-Boon Yar said:

    You probably meant:

    NSAssert([NSThread isMainThread], @”Not on the main thread”);

    instead of:

    NSAssert([NSThread mainThread], @”Not on the main thread”);

Leave your response!

Add your comment below, or trackback from your own site. You can also subscribe to these comments via RSS.

Be nice. Keep it clean. Stay on topic. No spam.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

This is a Gravatar-enabled weblog. To get your own globally-recognized-avatar, please register at Gravatar.