Using the AWS Persistence Framework for Core Data
This article discusses a new beta framework to link Apple's Core Data to Amazon DynamoDB.
Submitted By: BobK@AWS
AWS Products Used: AWS SDK for iOS, Amazon DynamoDB
Created On: May 23, 2012
Version 2 of the AWS Mobile SDK
- This article and sample apply to Version 1 of the AWS Mobile SDK. If you are building new apps, we recommend you use Version 2. For details, please visit the AWS Mobile SDK page.
- This content is being maintained for historical reference.
This article discusses a new framework that works with the AWS SDK for iOS to allow apps to use Amazon DynamoDB as a persistent back-end for the Apple Core Data Framework. The article assumes familiarity with both technologies. If you haven't done so already, please review the User Preferences sample app to familiarize yourself with using Amazon DynamoDB with the AWS SDK for iOS. You should also review Apples's Core Data Introduction.
The Apple Core Data Framework provides generalized and automated solutions to common tasks associated with object life-cycle and object graph management, including persistence. You can read more about Core Data in the Apple documentation here
Amazon DynamoDB is a fast, highly scalable, highly available, cost-effective, non-relational database service. Amazon DynamoDB removes traditional scalability limitations on data storage while maintaining low latency and predictable performance.
Overview
This sample app is based on Apple's example iOS Core Data app, Locations. Our version adds another entity to capture checkins at a particular location and adds a relationship between the two entities. Finally, we use the AWS Persistence Framework for Core Data to allow our app to save the entities to Amazon DynamoDB.
Entity and Relationship Model
Where the Locations example
from Apple has only a single entity, our version expands the model to have two entities,
Location and Checkin, with a 1-to-many relationship between
them.
Note: The AWS Persistence Framework for Core Data has been tested
with 1-to-1 and 1-to-many relationships.
For each object, we
defined the property types to correspond to an appropriate Core Data
type.
Note: The AWS Persistence Framework for Core Data supports String,
Numeric (Integer 16/32/64, Decimal, Double, and Float), Date, and Boolean types. For
properties that are hash keys, only String and Numeric types are supported. Primitives
are not supported as entity properties.
Configuring the AWS Persistence Framework for Core Data
Given this entity and relationship model, you need to configure the app to use the AWS Persistence Framework for Core Data.
The AppDelegate also implements the
AWSPersistenceDynamoDBIncrementalStoreDelegate
protocol, which requires
you to implement a single selector:
- (AmazonCredentials *)credentials { return [AmazonClientManager credentials]; } |
Note: This selector is called every time the framework requires a connection to Amazon DynamoDB; it should return valid credentials. The supplied sample uses TVM to gather these credentials.
The next step is to register that you want to use Amazon DynamoDB as your back-end store.
This happens in your AppDelegate as part of the persistentStoreCoordinator
selector.
[NSPersistentStoreCoordinator registerStoreClass:[AWSPersistenceDynamoDBIncrementalStore class] forStoreType:AWSPersistenceDynamoDBIncrementalStoreType]; persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; |
Next, you need to create a number of mappings to allow the framework to understand how to handle your managed objects. These include:
- Hash keys for each table in Amazon DynamoDB that is being
used to represent an entity.
NSDictionary *hashKeys = [NSDictionary dictionaryWithObjectsAndKeys: @"locationId", @"Location", @"checkinId", @"Checkin", nil];
- Attribute to use in each table to represent the
version
of the object being stored.NSDictionary *versions = [NSDictionary dictionaryWithObjectsAndKeys: @"version", @"Location", @"version", @"Checkin", nil];
- (Optionally) Table name to use for entities. By default, the
entity name is used.
NSDictionary *tableMapper = [NSDictionary dictionaryWithObjectsAndKeys: @"AWS-Locations", @"Location", @"AWS-Checkins", @"Checkin", nil];
With the mappings created, you next create a larger options dictionary and include the delegate for your object manager, which is set to be the AppDelegate itself.
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: hashKeys, AWSPersistenceDynamoDBHashKey, versions, AWSPersistenceDynamoDBVersionKey, self, AWSPersistenceDynamoDBDelegate, tableMapper, AWSPersistenceDynamoDBTableMapper, nil]; |
Finally, you pass the options dictionary to the persistence store coordinator and check for errors.
if(![persistentStoreCoordinator addPersistentStoreWithType:AWSPersistenceDynamoDBIncrementalStoreType configuration:nil URL:nil options:options error:&error]) { // Handle the error. NSLog(@"error: %@", error); } |
Fetching Managed Objects
When fetching objects in Core Data, you create an instance of the NSFetchRequest
class and specify the type of entity you are fetching:
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Location" inManagedObjectContext:managedObjectContext]; [request setEntity:entity]; |
While it is not shown in our sample app, you can also specify an NSPredicate
to
your NSFetchRequest
to restrict the entities returned. In the initial beta
release of the AWS Persistence Framework for Core Data, NSPredicate
is
supported only for hash key properties.
Note: Fetching without a predicate
will execute as many scan operations as necessary to load the entire contents of the
underlying Amazon DynamoDB table.
Once you've created your fetch request, you ask your managed object context to run it and return the results. You can then work with the returned array as you normally would.
NSArray *fetchedResults = [managedObjectContext executeFetchRequest:request error:&error]; NSMutableArray *mutableFetchResults = [fetchedResults mutableCopy]; |
Creating/Saving/Deleting Managed Objects
When creating new objects in Core Data, you must request they be created from the managed object context. After the object has been created, you can interact with it normally, setting properties on the object.
Checkin *checkin = (Checkin *)[NSEntityDescription insertNewObjectForEntityForName:@"Checkin" inManagedObjectContext:managedObjectContext]; checkin.checkinId = [Utilities getUUID]; checkin.checkinTime = [NSDate date]; checkin.comment = @"First Checkin"; |
Making changes to managed objects does not cause them to be persisted until the application
makes a call to explicitly save the changes.
Note: This saves any and all
changes to managed objects created or modified during this session or since the previous
save.
NSError *error; if (![managedObjectContext save:&error]) { // Handle the error. NSLog(@"error: %@", error); } |
The app can also choose to undo changes.
Note: It is possible to undo
persisted changes made during the current session, but you'll need to save the undo.
[[[self managedObjectContext] undoManager] undo]; |
Deleting objects also requires notifying the managed object context. You cannot remove objects from the back-end store until you've saved the changes.
[managedObjectContext deleteObject:locToDelete]; |
Conflict Management
When using the AWS Persistence Framework for Core Data on multiple threads or devices, you
may encounter a situation where you need to resolve inconsistencies between data on
Amazon DynamoDB tables and your local copy in memory. When the conflicts are detected,
NSManagedObjectContext
returns an NSError
object and your
change will not be persisted. You can change this behavior by setting a merge policy
to your NSManagedObjectContext
:
managedObjectContext.mergePolicy = [[[NSMergePolicy alloc] initWithMergeType:NSMergeByPropertyStoreTrumpMergePolicyType] autorelease]; |
There are five NSMergePolicyTypes available: NSErrorMergePolicyType
(default), NSMergeByPropertyStoreTrumpMergePolicyType
,
NSMergeByPropertyObjectTrumpMergePolicyType
, NSOverwriteMergePolicyType
, and
NSRollbackMergePolicyType
. The AWS Persistence Framework for Core Data can
handle all five merge policy types. For more information about
NSMergePolicy
, please read NSMergePolicy Class Reference.
Because NSManagedObject
instances returned by Core Data are lazily loaded, it is
possible that when you access an attribute of an object, the actual row in the Amazon
DynamoDB table has been already deleted. In this situation, the AWS Persistence
Framework for Core Data will return an empty NSManagedObject
(the accessor
will return nil) and sends out
AWSPersistenceDynamoDBObjectDeletedNotification
. You can register to
observe AWSPersistenceDynamoDBObjectDeletedNotification
by calling the
following method:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeDeletedObject:) name:AWSPersistenceDynamoDBObjectDeletedNotification object:nil]; |
When you receive the notification, you should remove any invalid objects from local cache and
refresh the NSManagedObject
instance as necessary. The notification object
has a userInfo
dictionary, which you can use to identify the deleted
object, containing:
- Hash key of the deleted object with
AWSPersistenceDynamoDBObjectDeletedNotificationHashKey
key - Entity name of the deleted object with
AWSPersistenceDynamoDBObjectDeletedNotificationEntityName
key -
NSManagedObjectID
of the deleted object withAWSPersistenceDynamoDBObjectDeletedNotificationObjectID
key
The following line shows how to refresh an NSManagedObject
. Please note that
refreshing an NSManagedObject
will send a getItem request to the Amazon
DynamoDB.
[managedObjectContext refreshObject:managedObject mergeChanges:YES]; |
Under heavily concurrent environments, periodically calling the refreshObject
method may reduce inconsistent states. Also, calling the refreshObject
method before saving the change will minimize inconsistent states to be persisted to the
Amazon DynamoDB tables.
Limitations
Although the AWS Persistence Framework for Core Data was developed to meet the widest use case possible, it does have certain limitations.
- iOS versions prior to 5.0 are not supported by the AWS Persistence Framework for Core Data.
- You cannot modify the hash key attribute once the object has been
persisted to the Amazon DynamoDB table. Please delete the object and
insert a new object instead of changing the hash key. When hash key
attributes are modified, the save operation will fail and return an
NSError
object. - Primitives are not supported as properties for entities. When
generating your
NSManagedObject
subclasses, make sure this option is not enabled. - For properties that are mapped to hash keys for the entities, only string and numeric (Integer 16/32/64, Decimal, Double, and Float) types are supported.
- For all other properties, it is possible to use Date and Boolean types as well as the already mentioned string and numeric types.
- Only 1-to-1 and 1-to-many relationships between entities are supported.
- The framework handles creating, modifying, and deleting rows in the underlying Amazon DynamoDB tables, but it does not manage the tables themselves. You must create tables using primitives provided by the AWS SDK for iOS or via the AWS Management Console.
- When fetching objects without a predicate, the framework will potentially do multiple scan operations against Amazon DynamoDB, which will use large amounts of your provisioned read throughput. Using a predicate when fetching uses a get request against Amazon DynamoDB, which uses only the throughput necessary to load the single object.
- With the current version of the framework,
NSPredicates
are limited to only the properties that are mapped to hash keys. - Save operations are not guaranteed to be transactional. You should check the errors that Core Data returns and formulate your handling strategies assuming partial data might have been persisted to DynamoDB tables.
- When doing fetches of all objects in a table, the AWS Persistence Framework for Core Data cannot guarantee that recent objects inserts and deletes will be reflected due to inconsistent read status.
- The Persistence Framework does not support sorting because the Amazon DynamoDB scan operation does not sort the result. However, Apple's NSManagementObjectContext Class Reference states that "if you fetch some objects, work with them, and then execute a new fetch that includes a superset of those objects, you do not get new instances or update data for the existing objects-you get the existing objects with their current in-memory state." In this situation, Core Data may sort the in-memory objects based on the sort descriptors provided. However, we don't recommend relying on this behavior; instead, you should sort the results in memory when the objects need to be sorted.
- With the current version of the framework, NSPredicates are limited
to only the properties that are mapped to hash keys. Also, when using
the framework, your query can specify only one hashKey and only one
possible value for that hashKey. Some examples are shown below:
"hashKey1=value1" supported, one hashKey and one value "hashKey1 = value1 OR hashkey1 = value2" not supported, one haskKey, but two values "hashKey1 IN (value1, value2)" not supported, one haskKey, but two values
Conclusion and Additional Resources
The code in this article demonstrates how to use the AWS Persistence Framework for Core Data to create a simplified object model backed by a scalable cloud database. We are offering the AWS Persistence Framework for Core Data as beta software that you can use in addition to the AWS SDK for iOS. You can download the SDK and review the documentation here.
For more information about using AWS credentials with mobile apps, see the following article:
Want to learn more?
To learn about mobile development best practices, follow our AWS Mobile Development Blog. You can also ask questions or post comments in the Mobile Development Forum about this or any other topic related to mobile development with AWS.