I create an category object and save it:
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
_category = (Category *)[NSEntityDescription
insertNewObjectForEntityForName:#"Category"
inManagedObjectContext:managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
then edit the name of the category object and save again.
_category.name = _nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
and get this error:
2013-01-12 17:53:11.862 instacat[7000:907] Unresolved error Error Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x2027b300 {NSAffectedObjectsErrorKey=(
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}, {
NSAffectedObjectsErrorKey = (
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
);
NSUnderlyingException = "Cannot update object that was never inserted.";
}
Thank you for your time and consideration.
I am using the AFIncrementalStore.
How about something like this:
self.category.name = self.nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
if(![self.category isInserted])
{
[managedObjectContext insertObject:self.category];
}
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
Basically check the object is it has been inserted before, if not, insert it and then save the context.
When you update an object, you can't use insertNewObjectForEntityForName, you need to first save your object, then call something like
[self.managedObjectContext refreshObject:_category mergeChanges:YES]
Then use managedObjectContext save again.
This is the difference in direct SQL as "INSERT" and "UPDATE".
Your object is loosing the managedObjectContext. Either use
self.managedObjectContext
or refetch the object in
[[FTAppDelegate sharedAppDelegate] managedObjectContext]
and edit the refetched object and then save it.
I have the same error but different and rare scenario, it happens once in almost 100 attempts. Find my problem below:
I have 2 NSManagedObjects in core data model:
1- Lead
2- LeadAttirbute
Lead has 1-M relationship with LeadAttribute.
There is a form that inputs for lead and refresh(create new lead) the form after submitting a lead. If i keep on submitting the leads then at a stage, [managedObjectContext save:&error]; starts giving below error:
Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x1f251740 {NSAffectedObjectsErrorKey=(
" (entity: LeadInfoAttribute; id: 0x1f2eb920 ; data: {\n attributeId = 0;\n lead = nil;\n optional = nil;\n orderId = 0;\n title = nil;\n value = Bjjbjp;\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}
And it keeps on giving the same error until i dont terminate and re-launch the app. I'm not able to update anything in core data model after this error occur, So my questions are:
1- Can we remove the fault state of core data? i.e to capture and delete the object that is creating trouble before making the save call again.
2- What could be the possible reasons for this issue? Since its very rare and can't reproduce this everytime.
I've just run into this issue and in my case the problem was following:
1) create new managed object in context A
2) save the context A
3) retrieve this object by objectID from context B
4) make changes on managed object and save the context B
Normally it wouldn't be a problem, but in this case the context A is child context and therefore doesn't save to persistent store (just to parent context, which isn't the context B). So when fetch for managed object is done from context B, context doesn't have this object. When changes are made, context tries to save them anyway...and thats when this error occurs. In some cases (as #Trausti Thor mentioned) the refreshObject:mergeChanges: method could help (it passes the data to another context).
In Your case I'll check if:
1) managed object context from [[FTAppDelegate sharedAppDelegate] managedObjectContext] returns always the same context
2) when you save the category, check if it was really saved to persistent store (self.category.objectID.isTemporaryID == NO)
NOTE:
The second point is more important, because if you look carefully, your category object still has temporary objectID, that means it's not persisted.
What I think it's happening is that you are not getting the right NSManagedObjectContext.
Think about it as a session. So when you update you are not getting the right session and so your object doesn't exist there.
Before doing the second save try to find your object on that NSManagedObjectContext.
If you need further help please describe what happens between the creation and the update.
Getting the wrong NSManagedObjectContext can be due to bad code on the AppDelegate or accessing from another thread other than the main thread.
Related
I have two entities (Categories, Event) and they have two-way many-to-many relationship. A Category can have more than one Event and an Event can have more than one Category. Category to Event relationship is optional in a sense that Category can exist without an Event, but Event to Category relationship is mandatory (Event cannot exist without Category). I'm trying to insert events and adding categories to them but I'm getting NSValidationErrorValue=Relationship error. This is my code :
private func storeEventsXMLStream(_ xml: XMLIndexer) {
let managedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.persistentStoreCoordinator
// Remove all data before inserting
// This line of code is necessary because data needs to be downloaded on daily basis.
// Otherwise, I will get redundant data.
removeAllExistingData("Event_Ottawa", managedObjectContext: managedObjectContext)
autoreleasepool { // Scoping is necessary to fix memory leak
for xmlcat in xml["events"]["event"]{
let event = NSEntityDescription.insertNewObject(forEntityName: "Event_Ottawa", into: managedObjectContext) as! Event_Ottawa
event.id = Int32((xmlcat.element?.attribute(by: "id")?.text)!)!
event.website_url_english = xmlcat["website_url_english"].element?.text
event.website_url_french = xmlcat["website_url_french"].element?.text
// setting other attributes of events here. Exactly like I did in above 3 line
// Just another attribute. Storing it a String in Coredata
var recur_rules = ""
for rule in xmlcat["recur_rules"]["recur_rule"] {
recur_rules += (rule.element?.attribute(by: "weekday")?.text)!
}
if !recur_rules.isEmpty {
event.recur_rules = recur_rules
}
do {
var predicateArray:[NSPredicate] = []
// Categories are inserted to the Coredata before this method call. So I'm fetching the applicable one here.
for category in xmlcat["categories"]["category"] {
let predicate = NSPredicate(format: "id = %#", (category.element?.attribute(by: "id")?.text)!)
predicateArray.append(predicate)
}
let requestCategory:NSFetchRequest<Category_Event_Ottawa> = Category_Event_Ottawa.fetchRequest()
requestCategory.predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicateArray)
let managedContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
managedContext.persistentStoreCoordinator = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.persistentStoreCoordinator
let applicableCategories = try managedContext.fetch(requestCategory)
for category in applicableCategories {
event.addToCategory(category)
}
}
} catch {
print(error)
}
}
}
// only save once per batch insert
do {
try managedObjectContext.save()
} catch {
print(error)
}
managedObjectContext.reset() <-- I get EXC_BAD_ACCESS, when I use the same object context for fetching as insertion
}
And the error I receive :
Error Domain=NSCocoaErrorDomain Code=1560 "(null)" UserInfo={NSDetailedErrors=(
"Error Domain=NSCocoaErrorDomain Code=1550 \"The operation couldn\U2019t be completed. (Cocoa error 1550.)\" UserInfo={Dangling reference to an invalid object.=null, NSValidationErrorValue=Relationship 'category' on managed object (0x6100050963f0)
If use the same managed object context for both insertion and fetching, I don't get this error anymore. But I get EXC_BAD_ACCESS from the line that reset context object. I learnt it from another post that CoreData is not thread safe. So maybe that was an issue. But how do I resolve this issue? In case it's relevant, Deletion Rule for Event-Category relationship is Nullify and Category-Event is Cascade.
In this line:
requestCategory.predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicateArray)
you're creating a compound predicate with ANDs. You're asking Core Data to fetch you all of the categories that have id = 7 AND id = 8 AND ... etc. That's not going to work. The category can only have a single id. You actually want an 'or' predicate in this case.
However, I think the better way to do this is to load all of your categories into a dictionary keyed by their id before you start looping through your XML, and then just pull them out of the dictionary. That will be much more performant than fetching each time.
Also, you can't fetch categories in a separate context and then create relationships between objects from separate contexts. Core Data will crash if you try it.
I've read some blogs on this but I'm still confused on how to use NSPersistentContainer performBackgroundTask to create an entity and save it. After creating an instance by calling convenience method init(context moc: NSManagedObjectContext) in performBackgroundTask() { (moc) in } block if I check container.viewContext.hasChanges this returns false and says there's nothing to save, if I call save on moc (background MOC created for this block) I get errors like this:
fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x17466c500) for NSManagedObject (0x1702cd3c0) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ... }fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x170664b80) for NSManagedObject (0x1742cb980) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ...} and new database row = {id = 2; ...}"
)}
So I've failed to get the concurrency working and would really appreciate if someone could explain to me the correct way of using this feature on core data in iOS 10
TL:DR: Your problem is that you are writing using both the viewContext and with background contexts. You should only write to core-data in one synchronous way.
Full explanation: If an object is changed at the same time from two different contexts core-data doesn't know what to do. You can set a mergePolicy to set which change should win, but that really isn't a good solution, because you will lose data that way. The way that a lot of pros have been dealing with the problem for a long time was to have an operation queue to queue the writes so there is only one write going on at a time, and have another context on the main thread only for reads. This way you never get any merge conflicts. (see https://vimeo.com/89370886#t=4223s for a great explanation on this setup).
Making this setup with NSPersistentContainer is very easy. In your core-data manager create a NSOperationQueue
//obj-c
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
//swift
let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1
And do all writing using this queue:
// obj c
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
[context performBlockAndWait:^{
blockCopy(context);
[context save:NULL]; //Don't just pass NULL here, look at the error and log it to your analytics service
}];
}]];
}
//swift
func enqueue(block: #escaping (_ context: NSManagedObjectContext) -> Void) {
persistentContainerQueue.addOperation(){
let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
context.performAndWait{
block(context)
try? context.save() //Don't just use '?' here look at the error and log it to your analytics service
}
}
}
When you call enqueueCoreDataBlock the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.
This is a really bizarre issue, and I thought I understood Core Data.
I use a background context that has no parent. Hooked right into the Persistent Store Coordinator. I update objects on this background context then save it. I listen to the ContextDidSaveNotification and merge those changes into my main thread context. Those updated objects are not faults on the main thread as they are already used to populate table view cells. So I would expect those changes to actually merge. But they are not.
Without getting into the details of my data models, it suffices to say that an object has a property "downloadState". Once the parsing work is done on the background thread, downloadStateValue (an enum) gets set to "3", which corresponds to 'completed'.
I subscribe to the ContentWillSave notification now to inspect what's going on. I get this at the end of my parsing work:
2016-06-13 10:19:21.055 MyApp[29162:52855206] Going to save background context.
updated:{(
<QLUserPinnedCourse: 0x7fe195403c10> (entity: QLUserPinnedCourse; id: 0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11> ; data: {
course = "0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55>";
courseId = 2794;
/* other fields redacted */
}),
<QLCourse: 0x7fe1954cded0> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
/* other fields redacted*/
contentDownloadState = 3;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The NSFetchedResultsController that is listenting to QLUserPinnedCourse objects gets the delegate calls, which triggers cell reloads in my tables.
The predicate is:
// Specify criteria for filtering which objects to fetch
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pinned == %# && course.contentDownloadState IN %#",
#YES,
#[#(QLDownloadStateSucceeded), #(QLDownloadStateNotYetAttempted), #(QLDownloadStateFailed), #(QLDownloadStateIncomplete)]
];
Now when I get to the cell code, I have a QLUserPinnedCourse object to work with. I set a breakpoint in the debugger and get:
(lldb) po userCourse.course
<QLCourse: 0x7fe19568f740> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
contentDownloadState = 1;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The question is, WHY is contentDownloadState not 3, but still 1 ?? I don't get it.
Shouldn't these changes have been merged??
Details as to my stack:
PSC -> Private Concurrent (saving context) -> Main Thread context
PSC -> Private Concurrent (import context)
ContextDidSave:
if the context was an import context, merge changes into both contexts above:
_contextSaveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note)
{
NSManagedObjectContext *contextSaved = note.object;
NSManagedObjectContext *moc = weakself.mainQueueContext;
// basically, if this was a background worker thread
DDLogDebug(#"updatedObjects:%#", note.userInfo[NSUpdatedObjectsKey]);
if ([contextSaved.userInfo[CoreDataUserInfoKeyIsWorkerContext] boolValue])
{
[weakself.privateSavingContext performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[weakself.privateSavingContext objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[weakself.privateSavingContext mergeChangesFromContextDidSaveNotification:note];
[moc performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[moc objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[moc mergeChangesFromContextDidSaveNotification:note];
}];
}];
}
}];
Note that I'm asking the userCourse.course object for its attribute, although my FRC is interested in QLUserPinnedCourse objects. I thought because I specify a keypath in the predicate that relates to a QLCourse object, these changes are refreshed.
It's a quirk of Core Data. You actually need to re-fault objects in the main context which were updated by the save operation.
Here's an example in Swift:
mainContext.performBlock {
let updatedObjects : Set<NSManagedObject> = notification.userInfo![NSUpdatedObjectsKey] as! Set<NSManagedObject>
for obj in updatedObjects {
self.mainContext.objectWithID(obj.objectID).willAccessValueForKey(nil)
}
self.mainContext.mergeChangesFromContextDidSaveNotification(notification)
}
The main part is the call to willAccessValueForKey:nil, which causes the object to be marked as a fault. This will cause NSFetchedResultsControllers in the main context to fire.
So I found a solution but I can't tell you why it works.
The problem I suppose is that I would have a method 'start downloading content' and I would update the property contentDownloadState on the main thread context to 'incomplete/downloading', then proceed to get all the content.
All the rest of the work was done on a background thread context. When finished I updated that value with 'succeeded'. It wasn't merging that change. I have no idea why.
Once I decided to do everything on the worker context. i.e. change its value then save the context to disk, the changes, ALL the changes were propagating.
So in the end I solved it, but really don't understand the problem.
I have been testing out CloudKit as i wish to release an app using it when the release of iOS8 occurs. It seems simple enough to save data using the code below:
CKRecordID * recordID = [[CKRecordID alloc] initWithRecordName:#"basicRecord"];
CKRecord * record = [[CKRecord alloc] initWithRecordType:#"basicRecordType" recordID:recordID];
[record setValue:#"defaultValue" forKey:#"defaultKey"];
CKDatabase *database = [[CKContainer defaultContainer] publicCloudDatabase];
[database saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"Record Saved!");
}
}];
and I receive no errors from this. However, if i try to run the code again, maybe because i have changed the record value to
[record setValue:#"newValue" forKey:#"defaultKey"];
I receive an error which begs the question, how do i go about saving a modified piece of data. After all, this is a fundamental part of saving things to the cloud. The error is below and any help would be greatly appreciated, don't hesitate to ask for further information.
Error: <CKError 0x17024afb0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x144684a80; basicRecord:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 182C497F-966C-418A-9E6A-5563BA6CC6CD; container ID = "iCloud.com.yourcompany.CloudKit">
This error is probably because saveRecord: works only for new records or records that are newer than the version on the server:
This method saves the record only if it has never been saved before or if it is newer than the version on the server. You cannot use this method to overwrite newer versions of a record on the server. CKDatabase docs
The recommended approach to modify an existing record (or set of records) is to use a CKModifyRecordsOperation set with the desired savePolicy to deal with conflicts:
After modifying the fields of a record, use this type of operation object to save those changes to a database. (...)
When saving records, the value in the savePolicy property determines how to proceed when conflicts are detected on the server. CKModifyRecordsOperation docs
From the docs of CKRecord:
New records exist only in memory until you explicitly save them to iCloud.
When you set the new value [record setValue:#"newValue" forKey:#"defaultKey"]; you have already saved the record, making it invalid.
You can use CKModifyRecordsOperation and in most situations it might be preferrable but you don't have to. Just fetch your data using a fresh CKRecord, then feed that record into saveRecord: as described here.
After you save the record, fetch it so that the retured record will then have the RecordID that Cloudkit added
Then on that same fetched record, use setValue to change the data you want to change
Then you can use CFModifyRecordsOperation
In the example below, cachedCKRecordsServiceCenter contains the fetched records from cloudkit and those records have the CloudKit RecordID's in them......
//find this service center in the cached records
for (_,serviceCenter) in (theModel?.cachedCKRecordsServiceCenter.enumerated())! //is data for logged in Co ONLY with NO Co name attached
{
let name = serviceCenter["name"] as! String
returnValue = "Try Again"
if name == displayedRecordName
{
serviceCenter.setValue(displayedRecordName! + "_" + (theModel?.companyName)!, forKey: "name") //db values have Co name appended
serviceCenter.setValue(label2Text.text, forKey:"street1")
serviceCenter.setValue(label3Text.text, forKey:"street2")
serviceCenter.setValue(label4Text.text, forKey:"city")
serviceCenter.setValue(label5Text.text, forKey:"state")
serviceCenter.setValue(label6Text.text, forKey:"zip")
serviceCenter.setValue(label7Text.text, forKey:"phone")
serviceCenter.setValue(label8Text.text, forKey:"email")
serviceCenter.setValue(label9Text.text, forKey:"note")
let saveRecordsOperation = CKModifyRecordsOperation()
var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray
ckRecordsArray.append(serviceCenter)
saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .ifServerRecordUnchanged
appDelegate.locked = true
saveRecordsOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if error != nil {
// Really important to handle this here
////////print("ERROR: Unable to update Driver Location: Error= \(error)")
self.returnValue = "ERROR: Unable to update Driver Location: ERROR = \(error)"
self.appDelegate.locked=false
}
else
{
////print("Successfully updated Service Center")
self.appDelegate.locked=false
self.returnValue = "Successfully updated Service Center"
self.appDelegate.locked=false
//reget the data into the cach
self.theModel?.fetchServiceCenterFromCloudKit1()
}
}
CKContainer.default().publicCloudDatabase.add(saveRecordsOperation)
}
}
I'm using Magical Record in my app, and want to add the functionality for a user to add a 'Note', which is a child of 'entry'.
I added this code:
[MagicalRecord saveWithBlock: ^(NSManagedObjectContext *localContext) {
Note *newNote = [Note MR_createInContext: localContext];
newNote.content = noteContent;
newNote.name = #"User Note";
[self.entry addNotesObject: newNote];
}
completion: ^(BOOL success, NSError *error) {
if (error != nil)
{
// show alert
}
else if (success)
{
[[self tableView] reloadData];
}
}];
The error I keep getting on the last line is "Illegal attempt to establish a relationship 'entry' between objects in different contexts"
I tried setting the context of both 'entry' and 'newNote' to 'localContext', but I still get the same error.
What am I missing?
self.entry was created in different context, so you can't access it from this one.
Instead of:
[self.entry addNotesObject: newNote];
you should first find self.entry object in localContext:
[[self.entry MR_inContext:localContext] addNotesObject: newNote];
You can find an explanation of using MagicalRecord in a concurrent environment at Performing Core Data operations on Threads. Though it's quite short, so in my opinion it's worthwhile to read Core Data Programming Guide even though you don't use CD directly.