8
Aug
2011
 

Transient Entities and Core Data

by Saul Mora

Core Data has many features, one of which is the Transient attribute. This type of attribute on a Core Data entity is part of a data model, but is not persisted in your Core Data persistent store. If you open up the raw SQLite store file and peek at the schema, you will not find any transient attributes. These transient attributes are still handy despite no data being persisted to the data store. One useful scenario is when a value is calculated based on other attributes. This calculation can be performed in an entity’s awakeFromFetch method, and stored at run time in this transient attribute.

One scenario I’ve been asked about on more than one occasion recently is that of temporary, or transient entities. It seems that more and more developers have a need for temporary, or transient instances of entities in your Mac or iOS apps. Having temporary object instances can be useful and necessary in certain situations. Unfortunately, Transient Entities do not technically exist within the Core Data framework, however there are simple solutions to add temporary, un-persisted, data within the context of Core Data. Let’s go over some methods to effectively use the concept of Transient, or more appropriately Temporary Entities in Core Data without direct built-in support.

Why even put them in Core Data in the first place?

This should be the first qustion you ask when you’re talking about going down the path of doing something with Core Data that it doesn’t already do for you. So, why not just use Plain Old Objective-C Objects … POOCOs anyone? There are a couple scenarios in which you would want even your temporary objects to be NSManagedObjects. The main situation is where you actually do persist the instance to the persistent store. Another case may be that your data objects are easily defined, modeled and imported through the Core Data toolset. That is, you have an entity defined and related to other entities in your object graph in your Model, and the import code is shared or related. In this case, it may be more maintainable to just let that all coexist. The most common reason is that you want to have a temporary object with the option to cancel and destroy it at any time. This is common in a “New Document” situtation where the user wants to enter new data, but decides to cancel half way in, and discard what’s already been entered.

Whatever the reason, it should be through a rigorous process that you have ultimately decided that your objects are better off as NSManagedObject instances and not just plain old Objective C objects with references to NSManagedObjects. In most cases you will be better off not even dealing with the complexity of temporary NSManagedObjects in the first place.

That being said, since you’ve come this far, it seems likely that maybe you really do have a good reason to have a temporary object instance as a Core Data object. Great, let’s go over three common, and easy to implement, solutions with code to show how to handle this as simply as possible. Also, we’ll go over the pros and cons of each approach as there are certainly tradeoffs which you must evaluate in order to choose the correct solution for your problem.

The code samples shown here will be using the helpers found in Magical Record, available on github.

Scratch Managed Object Context

Perhaps the simplest scenario in creating temporary NSManagedObjects is to create them in a temporary NSManagedObjectContext. Apple’s documentation commonly refers to the NSManagedObjectContext as a ‘scratch pad’ or ‘working area’ for changes to NSManagedObjects. When you create any NSManagedObject in a Managed Object Context, it is not perisisted to the store util you call save: on that context. By not calling save: on a second scratch context, any changes made will dissappear when the context, and the managed objects it contains, is dealloc’ed. Let’s take a look at implementing this example in code:

@interface MyViewController ()
@property (nonatomic, retain) NSManagedObjectContext *scratchContext;
@end
 
@implementation MyViewController
@synthensize scratchContext = scratchContext_;
- (void)viewDidLoad
{
  self.scratchContext = [NSManagedObjectContext context];
}
...
- (void)importData
{
  MyEntity *instance = [MyEntity createInContext:self.scratchContext];
  // data initialization code
  // don't call save:
  [[NSManagedObjectContext defaultContext] save]; // won't save anything in the scratch context
}
@end

In this first example, we have two NSManagedObjectContext objects, the first is created for you when you use one of MagicalRecord’s setup methods. As described in a previous post, this first context is set as the default context for all Core Data operations. However, in this scenario, we want to create a second context that uses the same persistent store. Using MagicalRecord, this is as simple as using the context method. This will create a new NSManagedObjectContext instance, and initialize it with the default NSPersistentStoreCoordinator. This is all that’s necessary to create a usable scratch context. Now, instead of creating entities in the default context, perform them in this scratch context. This is what’s happening in the importData method. By creating a new entity in the scratch context, we’re making a temporary object that potentially can be thrown away when we’re done with the view controller.

In Memory SQLite Persistent Store

One of the useful features of SQLite is that it can also persist simply in memory, or system RAM, only. This feature translates to Core Data persistent stores quite nicely as it’s a simple configutation change to the addPersistentStore: call when setting up your NSPersistentStoreCoordinator instance.  To simplify things, while adding this option, we’re going to go over adding an in-memory SQLite store to the same default NSPersistentStoreCoordinator.  This route may not be the common route in iOS apps, but Core Data is set up to handle the mapping and complexity of multiple stores for you, so it makes sense to use that functionality for this solution to try to keep the code as simple as possible.  Let’s take a look at some code:

@interface SampleAppDelegate <UIApplicationDelegate>
@property (nonatomic, retain, readonly) NSPersistentStore *inMemoryStore;
@property (nonatomic, retain, readonly) NSPersistentStore *fileBasedStore;
@end
 
@implementation SampleAppDelegate
@synthesize inMemoryStore = inMemoryStore_;
@synthesize fileBasedStore = fileBasedStore_;
- (void)awakeFromNib
{
    [MagicalRecordHelpers setupDefaultCoreDataStack];
    self.fileBasedStore = [[[NSPersistenStoreCoordinator defaultCoordinator] persistentStores] objectAtIndex:0];
    self.inMemoryStore = [[NSPersistentStoreCoordinator defaultCoordinator] addInMemoryStore];  //configure in memory store<br />
}
...
@implementation MyViewController
//assign new object to in memory persistent store
- (MyEntity*)createTemporaryEntity
{
    MyEntity *newInstance = [MyEntity createEntity];
    NSManagedObjectContext *context = [NSManagedObjectContext defaultContext];
    NSPersistentStore *inMemoryStore = [(SampleAppDelegate *)[[UIApplication sharedApplication] delegate] inMemoryStore];
    [context assignObject:newInstance toPersistentStore:inMemoryStore];
    return newInstance;
}
@end

We’ve again use the MagicalRecord helpers in this second example to create an in-memory SQLite store on the default NSPersistentStoreCoordinator. Notice that we also must keep a reference to it in our app delegate. The reason for this reference is easy to understand once we get to creating temporary object instances in the createTemporaryEntity method on MyViewController. Creating an entity is as straight forward as always, however, we must then assign this new entity to the in-memory store using our saved reference. We must also assign our non-temporary objects to the fileBasedStore as well.

There is one rather large caveat to this solution–on iOS devices in particular. There is a finite limit to what you can store in memory during the lifetime of an app. Going over this limit can be grounds for termination by the system watchdog process. If you implement this solution and find that you are constantly hitting a memory limit in your app due to the temporary in memory store, a simple variant to this solution is to make this a regular disk based SQLite store, but make it a secondary store, one that can easily be deleted by some clean up code later on. By persisting the store to disk, you don’t have the same constant memory pressure, and you also still have a secondary store which only contains temporary instances of your objects.

Mark Entities for deletion, and delete later

In our third scenario, the Core Data stack remains the same as in our original simple Core Data stack setup. However, this situation is helpful when objects are being used by the UI. In some cases, references to managed objects in the UI may have a reason to delay the release of these objects. In order to avoid crashes due to deleting the persistence information out from under the object, we can simply say that entity should be deleted later. MagicalRecord provides a truncateAll (and the truncateAllInContext: variant) to remove all the instances of a particular type from the store. However, in this case, we need to have a more specific filter on a flag on the entity.

[MRCoreDataAction saveDataWithBlock:^(NSManagedObjectContext *localContext)
{
    NSPredicate *itemsToDeleteSearch = [NSPredicate predicateWithFormat:@"shouldBeDeleted = YES"];
    NSArray *itemsToDelete = [SampleEntity findAllWithPredicate:itemsToDeleteSearch];
    for (NSManagedObject *entity in itemsToDelete)
    {
        [entity deleteInContext:localContext];
    }
}];

In the case where the shouldBeDeleted flag is a transient attribute, using an NSPredicate search against the store will not work as this attribute is not even created in the SQLite schema for that entity. A variant on this solution is to hang on to those deleted references in an NSMutableArray (or NSMutableSet) and iterate on that collection at the appropriate time. However, this variant may backfire in the event of a crash or a forced termination. That is, in either of those scenarios, your collection of instances to delete will no longer be in memory.

On iOS 4 and above, applications can enter the “background”. This is a perfect time to perform this delayed purge as it can be done without the user really noticing any UI jerkiness caused by merges. Heck, if you make other apps slow down, the user may just blame that one! (Don’t do this…be a good app citizen).

Conclusion

I’ve just described three ways to deal with temporary Core Data objects. These are by no means the only ways to handle transient entities, however, these are relatively simple concepts. The code for each of these examples is also simple enough to retrofit into existing application code. If you have other solutions relating to the idea of temporary storage, I’d be happy to hear about them in the comments below!