Hello guys I am developing an app which uses coredata (multithreaded), below is the coredata stack used (this was designed via a tutorial found here: https://www.cocoanetics.com/2012/07/multi-context-coredata/)
THE MODEL
- (NSManagedObjectModel *)managedObjectModel {
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"XXX" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
THE MAIN CONTEXT
- (NSManagedObjectContext *)managedObjectContext{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.parentContext = [self writerManagedObjectContext];
return _managedObjectContext;
}
THE WRITER CONTEXT
- (NSManagedObjectContext *)writerManagedObjectContext{
if (_writerManagedObjectContext != nil) {
return _writerManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _writerManagedObjectContext;
}
THE PERSISTENT STORE COORD
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"XXX.sqlite"];
NSError *error = nil;
NSString *failureReason = #"There was an error creating or loading the application's saved data.";
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:#"XXX" URL:storeURL options:options error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
As you can see, the WRITER context is the parent of the MAIN context which means that the WRITER would handle saving data to the store whilst it also updates the MAIN context (in memory) of any changes.
NOTE: the WRITER save to the store because the POS was set to the WRITER context.
On the entities in the model, I set 'Id' as a unique constraint for all entities (tables) in the database which is used for UPSERT (i.e. an equivalent of SQL insert OR replace).
And both the MAIN and WRITER contexts have their merge policies set to NSMergeByPropertyObjectTrumpMergePolicy which ensures objects between the WRITER, the MAIN and the store are in sync.
[[CoreDataCommons mainContext] setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[[CoreDataCommons writerContext] setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
Now when I want to insert to the database I create a new context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:writerManagedObjectContext];
[context performBlock:^{
// Populate entity attributes and save
for (id object in objects){
// Save every 500 objects
if (count % 500 == 0){
if ([context save:&error]){
}
}
count++;
}
// Final save if anything unsaved
if ([context save:&error]){
}
// Save WRITER context which will push changes to MAIN context
[writerManagedObjectContext performBlock:^{
if (![writerManagedObjectContext save:&writerError]) {
DLog(#"Writer: Unresolved error %#, %#", writerError, [writerError localizedDescription]);
}];
}];
THE PROBLEM
When the app is loaded for the very first time, it loads about 15000 objects (type JSON) from API and it writes this data in about 3 seconds (which I think is a bit too long BUT NOT THE MAIN ISSUE); however the first time load is not the issues. The ISSUE arises from subsequent loads from API; so, the second time it loads it takes about 5 mins to write the same data and it also BLOCKS THE MAIN THREAD.
After a few debugging I found that the constraint (i.e. the UPSERT) causes it to take too long to save when there is data already in the database. DOES THIS MEAN THAT ITS ALSO DOING THE PRIMITIVE
IF (EXIST){ // UPDATE}ELSE{ // INSERT}
I have used multiple context to ensure this is not the case but it still seems to hold the main thread and takes a ridiculous amount to time to save.
Questions
Firstly, could there be any issue caused by the coredata stack (i.e. a deadlock,background process etc)?
Secondly: Is this time taken to save the object normal with coredata? if not could anyone suggest an optimisation strategy.
Thirdly I am considering bypassing coredata and using sqlite directly. Any foreseen obstacles with this? apart from security layer provided by coredata.
Any ideas are welcome.
Thanks in advance.
Related
anyone can describe me about core data?
I want to create worksheet which store day activity record and that data stored in local file.
I think core data is best to store locally.
Thanks in advance.
You should see CoreData not as a database, but as a way to manage a graph of object.
You can then store this graph in different places (transparently from the application point of view) such as memory, XML, sqlite, and I think custom binary file.
What you usually do is to write the model in a core data model.
Each object is either an instance of NSManagedObject (which you can query / work with with methods such as valueForKey:, setValueForKey: etc) or subclasses of that class. This subclasses can be autogenerated directly from Xcode, and at this point you almost forget you are working with CoreData. Every attribute is a #property, every to-many relationship is a NSSet.
You get back to the fact that you are using CoreData when you create and want to save the object. In this case you have to get the 'context' in which the object resides, and call method on it (e.g. save)
There is full of tutorial and documentation on the web about CoreData.
In my opinion the core point is.. don't think at it as a relational database. "Be more object oriented" :)
To getting started you can take a look at:
http://www.raywenderlich.com/934/core-data-tutorial-for-ios-getting-started
The for more complex stuff the apple doc is ok
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/cdProgrammingGuide.html#//apple_ref/doc/uid/TP30001200-SW1
Yes you are right, Core data is bast way to store data in iOS applications.
With new XCode all you need to do is when creating new project click check box that you will use coreData.
This will create yourPorject.xcdatamodeld file and some methods inside your AppDelegate file :
- (NSURL *)applicationDocumentsDirectory {
// The directory the application uses to store the Core Data store file. This code uses a directory named "result.PingPong" in the application's documents directory.
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
- (NSManagedObjectModel *)managedObjectModel {
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"PingPong" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it.
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
// Create the coordinator and store
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"PingPong.sqlite"];
NSError *error = nil;
NSString *failureReason = #"There was an error creating or loading the application's saved data.";
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
// Report any error we got.
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSLocalizedDescriptionKey] = #"Failed to initialize the application's saved data";
dict[NSLocalizedFailureReasonErrorKey] = failureReason;
dict[NSUnderlyingErrorKey] = error;
error = [NSError errorWithDomain:#"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
#pragma mark - Core Data Saving support
- (void)saveContext {
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
NSError *error = nil;
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
(I case it didn't)
After that you create your entity object inside yourPorject.xcdatamodeld, by clicking on AddEntity, name it and inside with Add Attribute add all your attributes.
After that click on menu : Editor -> Create NSManagedObject subclasses. This will automatically create object for you.
All you need to do to save object into database is
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = appDelegate.managedObjectContext;
YourObject * o = [NSEntityDescription insertNewObjectForEntityForName:#"YourObject" inManagedObjectContext:context];
o.attribute1 = attribute1;
o.attribute2 = attribute2;
[context save:nil];
To fetch all object you will need this :
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = appDelegate.managedObjectContext;
NSError *fetchError;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"YourObject" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&fetchError];
if (fetchError != nil) {
NSLog(#"Error fetching database request. Reason: %#", fetchError);
}
I hope it will help you for start.
Marko
I'm new to Objective C, iOS and CoreData and I'm currently trying to save some data to my Entity in CoreData.
When trying to find the entity with insertNewObjectForEntityForName, it looks like it is returning nil based on the error below.
Error:
2015-04-06 17:46:12.274 Bugland[18623:607] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'Scores''
LeaderboardViewController.m
JPAppDelegate *JPAppDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context =[JPAppDelegate managedObjectContext];
NSManagedObject *newScore;
newScore = [NSEntityDescription
insertNewObjectForEntityForName:#"Scores"
inManagedObjectContext:context];
[newScore setValue: scoreToAddAsString forKey:#"score"];
NSError *error;
[context save:&error];
Ok, so, not that the app delegate should own the core data stack, but at the moment you have it doing so, and the instance that owns the core data stack needs to create the stack - at the moment nothing is doing that.
All you have is
#synthesize managedObjectContext = _managedObjectContext;
#synthesize managedObjectModel = _managedObjectModel;
#synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
which just creates some accessor methods. Nothing ever sets those instance variables.
You should have some code like the below behemoth, see also this.
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [NSManagedObjectContext new];
[_managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return _managedObjectContext;
}
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
return _managedObjectModel;
}
/**
Returns the URL to the application's documents directory.
*/
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
/**
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;
}
// copy the default store (with a pre-populated data) into our Documents folder
//
NSString *documentsStorePath =
[[[self applicationDocumentsDirectory] path] stringByAppendingPathComponent:#"Recipes.sqlite"];
// if the expected store doesn't exist, copy the default store
if (![[NSFileManager defaultManager] fileExistsAtPath:documentsStorePath]) {
NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:#"Recipes" ofType:#"sqlite"];
if (defaultStorePath) {
[[NSFileManager defaultManager] copyItemAtPath:defaultStorePath toPath:documentsStorePath error:NULL];
}
}
_persistentStoreCoordinator =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// add the default store to our coordinator
NSError *error;
NSURL *defaultStoreURL = [NSURL fileURLWithPath:documentsStorePath];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:defaultStoreURL
options:nil
error:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
// setup and add the user's store to our coordinator
NSURL *userStoreURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"UserRecipes.sqlite"];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:userStoreURL
options:nil
error:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
in a core data app with a one-to-many relationship (one "test", many "measures"), I used to have this code :
In AppDelegate.m :
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m :
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id contextDelegate = [[UIApplication sharedApplication] delegate];
if ([contextDelegate performSelector:#selector(managedObjectContext)])
context = [contextDelegate managedObjectContext];
return context;
}
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:context];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
}
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
It works great, but of course, the [NSKeyedArchiver archivedDataWithRootObject:plotDataArray]; can take a few seconds and block the UI so I would like to do it in background.
I spent a few hours to read everything about the concurrency in core data (and I am quite new at it), but I didn't find anything regarding my problem : how to deal with a one-to-many relationship background save ?
What I've tried so far :
In AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
//_managedObjectContext = [[NSManagedObjectContext alloc] init];
//[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = context;
[temporaryContext performBlock:^{
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:temporaryContext];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
// push to parent
NSError *error;
if (![temporaryContext save:&error])
{
// handle error
NSLog(#"error");
}
// save parent to disk asynchronously
[context performBlock:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error;
if (![context save:&error])
{
// handle error
NSLog(#"error");
}
}];
}];
}
}
Of course, I receive a SIGABRT error as "test" and "measure" are not in the same context...I've tried a LOT of different things, but I'm really lost.
Thanks in advance for any help.
I see two questions here: background asynchronous saving and what to do with objects in different contexts.
First about saving. Are you sure that it is saving itself that blocks your UI thread and not call to archivedDataWithRootObject? If saving itself is relatively fast, you can consider calling only archivedDataWithRootObject on a background queue, and then communicating the results back to the main queue where you’ll do the save on your UI context.
If it is still the save that takes too long, you can use the approach for background asynchronous saving recommended by Apple. You need two contexts. One context – let’s call it background – is of private queue concurrency type. It is also configured with persistent store coordinator. Another context – let’s call it UI – is of main queue concurrency type. It is configured with background context as a parent.
When working with your user interface, you’re using the UI context. So all managed objects are inserted, modified, and deleted in this context. Then when you need to save you do:
NSError *error;
BOOL saved = [UIContext save:&error];
if (!saved) {
NSLog(#“Error saving UI context: %#“, error);
} else {
NSManagedObjectContext *parent = UIContext.parentContext;
[parent performBlock:^{
NSError *parentError;
BOOL parentSaved = [parent save:&parentError];
if (!parentSaved) {
NSLog(#“Error saving parent: %#“, parentError);
}
}];
}
The save of the UI context is very fast because it doesn’t write data to disk. It just pushes changes to its parent. And because parent is of private queue concurrency type and you do the save inside performBlock’s block, that save happens in background without blocking the main thread.
Now about different managed objects in different contexts from your example. As you discovered, you can’t set an object from one context to a property of an object in another context. You need to choose a context where you need to do the change. Then transfer NSManagedObjectID of one of the objects to the target context. Then create a managed object from ID using one of the context’s methods. And finally set this object to a property of another one.
Essentially you are on the right track, but missing a couple of key elements;
Firstly you will need to transfer test from your main context to the secondary - this is done in the following way;
//this is the object saved in your main managedObjectContext;
NSManagedObjectID *currentTest = test.objectID;
creating the secondary context for adding your related objects can be performed on a background thread. You can use and NSBlockOperation to do the secondary save and create the context at the same time.
here is a simple example using the standard person / address example wired to an IBAction
- (IBAction)button1Click:(id)sender {
NSError *saveError = nil;
// create instance of person to save in our primary context
Person *newParson = [[Person alloc]initIntoManagedObjectContext:self.mainContext];
newParson.name = #"Joe";
[self.mainContext save:&saveError];
//get the objectID of the Person saved in the main context
__block NSManagedObjectID *currentPersonid = newParson.objectID;
//we'll use an NSBlockOperation for the background processing and save
NSBlockOperation *addRelationships = [NSBlockOperation blockOperationWithBlock:^{
// create a second context
NSManagedObjectContext *secondContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[secondContext setPersistentStoreCoordinator:coordinator];
NSError *blockSaveError = nil;
/// find the person record in the second context
Person *differentContextPerson = (Person*)[secondContext objectWithID:currentPersonid];
Address *homeAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
homeAddress.address = #"2500 1st ave";
homeAddress.city = #"New York";
homeAddress.state = #"NY";
homeAddress.zipcode = #"12345";
Address *workAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
workAddress.address = #"100 home Ave";
workAddress.city = #"Newark";
homeAddress.state = #"NJ";
homeAddress.zipcode = #"45612";
[differentContextPerson addAddressObject:homeAddress];
[differentContextPerson addAddressObject:workAddress];
[secondContext save:&blockSaveError];
}];
[addRelationships start];
}
in the above initIntoManagedObjectContext is a simple helper method in the NSManagedObject subclass as follows;
- (id)initIntoManagedObjectContext:(NSManagedObjectContext *)context {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:context];
self = [super initWithEntity:entity insertIntoManagedObjectContext:context];
return self;
}
An important note from Apple docs regarding NSBlockOperation:
You must create the managed context on the thread on which it will be used. If you use NSOperation, note that its init method is invoked on the same thread as the caller. You must not, therefore, create a managed object context for the queue in the queue’s init method, otherwise it is associated with the caller’s thread. Instead, you should create the context in main (for a serial queue) or start (for a concurrent queue).
I have a app that is using core data. The Core Data stack (the context, object graph, persistent store coordinator, and the persistent store) is being created, and I am able to use it without issue. The problem is that the saved data is not persisting, can someone help me with what I am doing wrong? Here is where I create the Core Data stack.
- (void)initializeCoreDataStack
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Favorites"
withExtension:#"momd"];
if (!modelURL)
NSLog(#"MODEL URL NOT INITIALIZED");
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
if (!mom)
NSLog(#"OBJECT MODEL NOT CREATED");
NSPersistentStoreCoordinator * psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
if (!psc)
NSLog(#"PERSISTENT STORE COORDINATOR NOT CREATED");
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[moc setPersistentStoreCoordinator:psc];
self.managedObjectContext = moc;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSArray *directory = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask];
NSURL *storeUrl = [directory lastObject];
storeUrl = [storeUrl URLByAppendingPathComponent:#"Favorites.sqlite"];
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeUrl
options:nil
error:&error];
if (!store)
{
NSLog(#"ERROR CREATING STORE: %# %#", error.localizedDescription, error.domain);
// present error to user
}
else
{
dispatch_sync(dispatch_get_main_queue(), ^{
// do something once the stack is finished being created
NSLog(#"persistent store created");
});
}
});
}
You have to save core data explicitly, otherwise it won't persist. Not hard to solve, though.
In your controller implementation file (e.g. coreDataViewController.m), call this function when you want to save changes to core data
// add this call, whenever you want to save data
// e.g. responding to a UIButton event
[self saveCoreDataContext];
- (void)saveCoreDataContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
My personal experience is that, even if you called [moc save:error], you may not find the content saved while you are running the app from Xcode -> Build and Run on device. However, if you stop the Xcode from running the app, and launch the app from the device by clicking the App Icon, the content is actually persisted.
Just personal experience that I found through trial and error, hope that you see the same thing.
You have to save the MOC.
[mom save:nil];
There's a few things here that stand out to me as odd. First, why are you threading the creation of the NSPersistentStore? Generally, I create the NSPersistentStoreCoordinator, add NSPersistentStores, and then create the contexts. I would suggest doing it in that order unless you have a good reason to do otherwise. While it's not specifically prohibited, you may experience problems adding a NSPersistentStore after the NSManagedObjectContext has been created.
I'm not sure if it is required or not, but I've always explicitly held a strong reference to the NSPersistentStoreCoordinator. It's useful to create multiple contexts, as well. I would suggest doing so in your code.
As everyone else has said, you also need to explicitly save.
I'm stuck on this problem with CoreData and Parent-Child MOCs: when adding objects to child MOC, saving them and saving the parent MOC all the objects gets their attributes reset to defaultValue.
I pasted here the logs from the two MOCs, specifically are the "stringAttribute" and "date" attributes that in these log are reset.
I searched for this problem everywhere but i didn't find anything, I also looked at lots of implementation of Parent-Child MOCs but I can't figure out what I'm doing wrong.
Thanks in advance!
Here's the code snippets:
I add some NSManagedObject to the main context and then save with saveContext: method
// Another singleton method
- (void)anotherMethod
{
[...]
[self.managedObjectContext insertObject:managedObject];
NSError *error;
save = [self saveContext:&error];
[...]
}
// Database manager singleton method
- (BOOL)saveContext:(DKError *__autoreleasing *)error
{
__block BOOL save = NO;
__block NSError *internalError;
[self.managedObjectContext performBlockAndWait:^{
internalError = nil;
[self.managedObjectContext log]; // See log 1.1 below
save = [self.managedObjectContext save:&internalError];
if (!save) {
NSLog(#"Error saving data in main context");
} else {
[self.managedObjectContext.parentContext performBlock:^{
internalError = nil;
save = NO;
[self.managedObjectContext.parentContext log]; // See log 1.2 below
save = [self.managedObjectContext.parentContext save:&internalError];
if (!save) {
NSLog(#"Error saving data to disk!");
}
}];
}
}];
*error = [DKError errorWithNSError:internalError]; // Custom error class
return save;
}
Parent - Child contexts code
- (NSManagedObjectContext *)writerObjectContext
{
if (_writerObjectContext != nil)
return _writerObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_writerObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerObjectContext setPersistentStoreCoordinator:coordinator];
}
return _writerObjectContext;
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:[self writerObjectContext]];
return _managedObjectContext;
}
Log 1.1
Inserted objects:
{(
<Entity: 0x9595120> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-07-12 10:36:31 +0000";
stringAttribute = ricercaEntity;
[...]
})
)}
Log 1.2
Inserted objects:
{(
<Entity: 0xb53ce80> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-01-05 11:00:00 +0000";
stringAttribute = nil;
[...]
})
)}
UPDATE
I've should have mention that the managedObject added to the context is initialized with context nil. Then before calling saveContext: I check for existence of object.managedObjectContext and if it's nil then I'll set that as [self managedObjectContext], the one created with the method above. So either if the managedObject is created with nil context, or created with:
+ (id)newObjectForInsertion
{
return [[self alloc] initWithEntity:[self entityDescription] insertIntoManagedObjectContext:[DKDatabaseManager defaultManager].managedObjectContext];
}
the associated managedObjectContext is on the same queue (NSMainQueueConcurrencyType).
Otherwise if the managedObject is create with +newObjectForInsertion all of the saveContext: concurrency-chain return YES and all the changes are passed to parent context.
I don't know if it's a bug or the way CoreData should work.
Same problem on Apple Developer Forums:
https://devforums.apple.com/thread/174677?tstart=90
You should init context with concurrencyType:
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
Also, set merge policy
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
NSMergeByPropertyObjectTrumpMergePolicy
This policy merges conflicts between the persistent store’s version of
the object and the current in-memory version, giving priority to
in-memory changes. The merge occurs by individual property. For
properties that have been changed in both the external source and in
memory, the in-memory changes trump the external ones.
Btw, I found similar question: strange-behavior-when-using-child-parent-nsmanagedobjectcontext look at the accepted answer which uses notifications to merge.