I saved my managedObjects. But my NSManagedObjectID is still temporary after saving. Why?
dispatch_async(privateQueue, ^{
__block NSMutableArray *ids = [NSMutableArray array];
[[[LPAppDelegate instance] privateContext] performBlockAndWait:^{
if ([responseObject isKindOfClass:[NSDictionary class]]) {
id arr = ((NSDictionary*)responseObject)[#"results"];
for (int i=0; i < ((NSArray *)arr).count; i++) {
LPFilm *film = [LPFilm MR_createEntityInContext:privateContext];
[ids addObject:film.objectID];
}
}
NSError *error = nil;
[privateContext save:&error];
if (error) {
NSLog(#"___fetch service error: %#", [error localizedDescription]);
}
}];
for (NSManagedObjectID *objID in ids) {
if (objID.isTemporaryID) {
NSLog(#"__tempID %#", objID);
}
}
});
When you save changes, new NSManagedObject instances get a new object ID. Previously existing instances of NSManagedObjectID are not converted in-place, they're replaced with new instances. But you have to ask the managed object for its new ID.
In your case, you're saving up an array of object IDs before saving. Those objects will never change, even if you save changes. But, if you go back to your managed objects and ask them for their object IDs again, you'll get different results, which will not be temporary.
Related
This is database design with to-many relations.This is my method
-(void)fetchChannelData:(id)responseObject{
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *channels = [responseObject objectForKey:#"channels"] ;
for (NSDictionary *channelStack in channels) {
NSLog(#"Tenant id %#",[[ NSUserDefaults standardUserDefaults ] valueForKey:#"test"]);
ChannelsData *channels = [ChannelsData MR_importFromObject:channelStack inContext:localContext];
[channels setCategory:[[channelStack valueForKey:#"category"] firstObject]];
[channels setTenantID:[[ NSUserDefaults standardUserDefaults ] valueForKey:#"test"]];
NSArray *channelLogo = [channelStack objectForKey:#"channelImages"] ;
for (NSDictionary *channelLogoStack in channelLogo) {
ChannelImages *images = [ChannelImages MR_importFromObject:channelLogoStack inContext:localContext];
[channels addImagesObject:images];
[images setChannelID:channels.channelID];
}
}
} completion:^(BOOL contextDidSave, NSError *error) {
if (contextDidSave) {
[self fetchListing];
}
}];
}
Where I am importing my data. I have dropdown where I selected different API points for specific data. All fetching different data.Now issue is that when I launch my app again service is again called and instead data is updated it insert it again.I already Used relatedByAttribute on each entity letting their "ID" be the primary key.]1]1
I have a problem checking whether a particular attribute of an Entity exists in the Core Data Database (through predicates) before creating a new object; if the object exists, I'd rather return it than create a new object.
I have a simple App which has a table view with a plus button in the Navigation Bar; the user clicks that and is presented with a View Controller with 4 text fields. They fill in that information, press save and it gets saved to Core Data and displayed in the TableView (through the use of NSFetchedResultsControllers).
The data model is as follows:
Transaction Entity with isReceived BOOL attribute
Person Entity with name string attribute
Occasion Entity with title string attribute
Item Entity with amount string attribute
The transaction has a relationship to the Person (whoBy), Occasion (Occasion) and Item entities.
In the view controller with the save method, I have the code below which will insert new objects into the Transaction, Person, Occasion Entities, etc. Each Transaction is of course unique, but with each transaction, the user can select an existing PERSON and/or Occasion and if that person then does not exist, it will be created (likewise with the occasion).
I'm slightly confused as to the format of the code here.
EDIT: I have tried a combination of code and can just not get this working. In the code below, I'm referencing person.name in the predicate, but I also tried creation a local NSString variable to hold the self.nameTextField.text code but that did nothing. I tried creating a NSString property to reference it that way and that not work. I tried using the words MATCHES, LIKE, CONTAINS, == and every combination in-between.
- (IBAction)save:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
Transaction *transaction= [NSEntityDescription insertNewObjectForEntityForName:#"Transaction" inManagedObjectContext:context];
Person *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
Occasion *occasion = [NSEntityDescription insertNewObjectForEntityForName:#"Occasion" inManagedObjectContext:context];
Item *amount = [NSEntityDescription insertNewObjectForEntityForName:#"item" inManagedObjectContext:context];
NSFetchRequest *personFind = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
personFind.predicate = [NSPredicate predicateWithFormat:#"name == %#", person.name];
// I have tried every combination of the predicate like MATCHES, LIKE.
// I created a local NSString variable and an NSString property
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
personFind.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:personFind error:&error];
if (!matches || ([matches count] > 1))
{
// Handle Error
}
else if ([matches count] == 0)
{
person.name = self.nameTextField.text;
transaction.whoBy = person;
occasion.title = self.occasionTextField.text;
transaction.occasion = occasion;
}
else
{
person = [matches lastObject];
transaction.whoBy = person;
occasion.title = self.occasionTextField.text
transaction.occasion = occasion;
}
if (![context save:&error])
{
NSLog(#"Can't save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
Logically, what I want to achieve is:
When the user is adding a Transaction, check if it's for a new person or an existing one — if it's an existing one, choose it from a list of Persons (and when the user selects a person, get its NSManagedObjectID). If it's a new one, create it on the spot.
The same for the Occasion.
Set all the other fields of the Transaction object (amount, etc.).
My question is:
What predicate do I use to get this working?
When I put a break point in this method, a NEW NAME (one that doesn't exist before) correctly calls the else if ([matches count] == 0) method and if I create an entry with an existing name, it calls the
else
{
person = [matches lastObject];
transaction.whoBy = person;
occasion.title = self.occasionTextField.text
transaction.occasion = occasion;
}
Even with the this statement, it is still creating a new person object for the same name.
I will correctly implement the occasion after getting the person working, but I'm just lost on how to get this working.
Any help would be massively appreciated!
"Is this correct?":
No. You are creating a new Person and Occasion objects whether you are using an existing person/occasion or not.
First check for existence and only if the object not already exist, insert a new one.
Alternatively, if the person/occasion exist, delete the inserted object.
"How do I retrieve the managedObjectID for person/event?":
Person* person = /*Get an existing person object*/
NSManagedObjectID* personId = person.objectID /*This is the person object ID, will work for any NSManagedObject subclass*/
To find a person that start with a string str use this predicate in a fetch request:
/*UNTESTED*/
[NSPredicate predicateWithFormat:#"(name BEGINSWITH[cd] %#)", str];
Edit:
To be more precise, you practice find or create using something like this:
(this is very limited, and only good for a single object performance-wise)
(NOT TESTED)
- (NSManagedObject*) findOrCreateObjectByValue:(id)value
propertyName:(NSString*)propertyName
entityName:(NSString*)entityName
additionalInfo:(NSDictionary*)additionalInfo
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSManagedObject* res = nil;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:entityName];
[r setPredicate:[NSPredicate predicateWithFormat:#"%K == %#",propertyName,value]];
NSArray* matched = [context executeFetchRequest:r
error:error];
if (matched) {
if ([matched count] < 2) {
res = [matched lastObject];
if (!res) { //No existing objects found, create one
res = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
[res setValue:value
forKey:propertyName];
[res setValuesForKeysWithDictionary:additionalInfo];
}
} else {
if (error) {
*error = [NSError errorWithDomain:#"some_domain"
code:9999
userInfo:#{#"description" : #"duplicates found"}];
}
}
}
return res;
}
So now, your save: method should look something like:
(I assume here that the person name and occasion title are held by a UITextField on the view controller [txtPersonName and txtOccasionTitle respectively] )
- (void) save:(id)sender
{
//create a clean context so that changes could be discarded automatically on failure
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setParentContext:[self managedObjectContext]];
//A Transaction is always created in save event, so add it to the context
Transaction* transaction = [NSEntityDescription insertNewObjectForEntityForName:#"Transaction" inManagedObjectContext:context];
__block NSError* error = nil;
Person* p = (Person*)[self findOrCreateObjectByValue:self.txtPersonName.text
propertyName:#"name"
entityName:#"Person"
additionalInfo:nil
context:context
error:&error];
if (!p) {
NSLog(#"Error: %#, person name: %#",error,self.txtPersonName.text);
return;
}
Occasion* o = (Occasion*)[self findOrCreateObjectByValue:self.txtOccasionTitle.text
propertyName:#"title"
entityName:#"Occasion"
additionalInfo:nil
context:context
error:&error];
if (!o) {
NSLog(#"Error: %#, occasion title: %#",error,self.txtOccasionTitle.text);
return;
}
transaction.whoBy = p;
transaction.occasion = o;
//Not sure what you are using this property for
transaction.item = [NSEntityDescription insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:context];
NSManagedObjectContext* ctx = context;
if ([context obtainPermanentIDsForObjects:[context.insertedObjects allObjects]
error:&error])
{
//save your changes to the store
__block BOOL saveSuccess = YES;
while (ctx && saveSuccess) {
[ctx performBlockAndWait:^{
saveSuccess = [ctx save:&error];
}];
ctx = [ctx parentContext];
}
if (!saveSuccess) {
NSLog(#"Could not save transaction, error: %#",error);
}
} else {
NSLog(#"Could not obtain IDs for inserted objects, error: %#",error);
}
//Do what you have to do next
}
This is just for making things a bit clearer on what you should do to avoid duplications, and reuse existing objects.
My Data Model is named "Person" and it has 3 attributes "id", "firstName", and "lastName"
When importing the JSON data using AFNetworking I want to be able to check whether the entity already exists or not within Core Data using the "id" as the identifier. If it isn't there I would like to create it, and if it is there I would like to merge the item or update it.
right now I have a method called duplicateCheck which looks like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"id==%#", _person.id];
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSError *error = nil;
[fetch setEntity:[NSEntityDescription entityForName:#"Person" inManagedObjectContext:self.managedObjectContext]];
[fetch setPredicate:predicate];
NSArray *items = [self.managedObjectContext executeFetchRequest:fetch error:&error];
for (NSManagedObject *object in items) {
// Not sure how to check from here and insert or update
// then save and call it during the API request?
}
I have a predicate set up but am not sure where to go from here. Is looping over each item the right way to go or am I going about this the wrong way?
Usually one would expect an identifier to be unique. therefor if the predicate return 0 objects, you know that this object is new. If 1 is returned you know that this object already exists and maybe you need to update it.
NSArray *items = [self.managedObjectContext executeFetchRequest:fetch error:&error];
if(items){
if([items count] == 0){
//the object is not present yet. create it.
} else if([items count] == 1) {
NSManageObject *obj = items[0];
//there is exactly 1 object. change it properties if needed
} else {
//what to do if several objects have the same identifier???
}
} else {
//handle error from the error object
}
So I commented with a link to a tutorial I wrote on this topic, but to narrow it down, this method may help guide you.
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
//
// Iterate over all registered classes to sync
//
for (NSString *className in self.registeredClassesToSync) {
if (![self initialSyncComplete]) { // import all downloaded data to Core Data for initial sync
//
// If this is the initial sync then the logic is pretty simple, you will fetch the JSON data from disk
// for the class of the current iteration and create new NSManagedObjects for each record
//
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
NSArray *records = [JSONDictionary objectForKey:#"results"];
for (NSDictionary *record in records) {
[self newManagedObjectWithClassName:className forRecord:record];
}
} else {
//
// Otherwise you need to do some more logic to determine if the record is new or has been updated.
// First get the downloaded records from the JSON response, verify there is at least one object in
// the data, and then fetch all records stored in Core Data whose objectId matches those from the JSON response.
//
NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:#"objectId"];
if ([downloadedRecords lastObject]) {
//
// Now you have a set of objects from the remote service and all of the matching objects
// (based on objectId) from your Core Data store. Iterate over all of the downloaded records
// from the remote service.
//
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
int currentIndex = 0;
//
// If the number of records in your Core Data store is less than the currentIndex, you know that
// you have a potential match between the downloaded records and stored records because you sorted
// both lists by objectId, this means that an update has come in from the remote service
//
for (NSDictionary *record in downloadedRecords) {
NSManagedObject *storedManagedObject = nil;
if ([storedRecords count] > currentIndex) {
//
// Do a quick spot check to validate the objectIds in fact do match, if they do update the stored
// object with the values received from the remote service
//
storedManagedObject = [storedRecords objectAtIndex:currentIndex];
}
if ([[storedManagedObject valueForKey:#"objectId"] isEqualToString:[record valueForKey:#"objectId"]]) {
//
// Otherwise you have a new object coming in from your remote service so create a new
// NSManagedObject to represent this remote object locally
//
[self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
} else {
[self newManagedObjectWithClassName:className forRecord:record];
}
currentIndex++;
}
}
}
//
// Once all NSManagedObjects are created in your context you can save the context to persist the objects
// to your persistent store. In this case though you used an NSManagedObjectContext who has a parent context
// so all changes will be pushed to the parent context
//
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Unable to save context for class %#", className);
}
}];
//
// You are now done with the downloaded JSON responses so you can delete them to clean up after yourself,
// then call your -executeSyncCompletedOperations to save off your master context and set the
// syncInProgress flag to NO
//
[self deleteJSONDataRecordsForClassWithName:className];
[self executeSyncCompletedOperations];
}
}
This is a SyncEngine from an RW tutorial. I need help understanding how only UPDATED records from the web are fetched and processed into Core Data.
- (void)processJSONDataRecordsIntoCoreData {
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// Iterate over all registered classes --- CHECK!
for (NSString *className in self.registeredClassesToSync) {
if (![self initialSyncComplete]) {
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
NSArray *records = [JSONDictionary objectForKey:#"results"];
for (NSDictionary *record in records) {
[self newManagedObjectWithClassName:className forRecord:record];
}
} else {
NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:#"objectId"];
if ([downloadedRecords lastObject]) {
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
int currentIndex = 0;
//if dl count is < current index, there is an updated object dl from the web
for (NSDictionary *record in downloadedRecords) {
NSManagedObject *storedManagedObject = nil;
//Quick check to see if they indeed match, if they do, update the stored object with remote service objects
if ([storedRecords count] > currentIndex) {
storedManagedObject = [storedRecords objectAtIndex:currentIndex];
}
//Othwerwise its a new object and you need to create a new NSManagedObject to represent it in CDdb
if ([[storedManagedObject valueForKey:#"objectId"] isEqualToString:[record valueForKey:#"objectId"]]) {
[self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
} else {
[self newManagedObjectWithClassName:className forRecord:record];
}
currentIndex++;
}
}
}
// After all NSMO are created in your context, save it!
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Unable to save context for class %#", className);
}
}];
// Cleanup time
[self deleteJSONDataRecordsForClassWithName:className];
[self executeSyncCompletedOperations];
}
[self downloadDataForRegisteredObjects:NO];
}
From what I understand, on the first or initial sync, it fetches JSONDictionaryForClassWithName which reads the downloaded data from disk and creates a newManagedObjectWithClassName.
My confusion is in the update else block. downloadedRecords is populated from JSONDataRecordsForClass which simply calls JSONDictionaryForClassWithName. Then it checks to see if there is at least 1 object in that array. If there is it does this:
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
This fetches all managedObjectsForClass:sortedByKey which is below:
- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds {
__block NSArray *results = nil;
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
NSPredicate *predicate;
if (inIds) {
predicate = [NSPredicate predicateWithFormat:#"objectId IN %#", idArray];
} else {
predicate = [NSPredicate predicateWithFormat:#"NOT (objectId IN %#)", idArray];
}
[fetchRequest setPredicate:predicate];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"objectId" ascending:YES]]];
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
}];
return results;
}
The next bit which compares the [storedRecords count] > currentIndex is confusing. Can someone please explain this? I think my confusion lies in what the managedObjectsForClass method does with the usingArraysOfIds & inArrayOfIds.
I would expect that at some point it gets the the updatedAt field from the downloaded records and compares it to the updatedAt field of the CoreData fetched records.
This function is processing the stored JSON. The actual remote fetching and updateAt checking happens in downloadDataForRegisteredObjects and mostRecentUpdatedAtDateForEntityWithName.
[storedRecords count] > currentIndex is a bit crazy. Although in defense of the original programmer, writing any decent syncengine will quickly make you go googoo. Basically he needs to work out which records are existing and which ones are new and update the local data store accordingly, that's all.
I had another look and this code is actually horribly broken. It will only works if either you have the same records both locally and remotely. Or if the new objects have an objectId that sort-wise comes after the last object the local store has. Which is not the case with Parse objectId's.
If you are testing with just one device this works because new objects will be inserted locally before being pushed to the server. Therefor you will always have the same amount of records. If additional records get inserted any other way, this code will do weird things.
I have a core data many to many relationship of article to category. When attempting to save, I get the following error. I am unable to find information about what it means, and why there are two versions when the database is empty. Can anyone shed some light?
Error: NSMergeConflict (0x76ae720) for NSManagedObject (0xd945560) with objectID '0xd943550 <x-coredata://09A438A8-E3F5-45FE-B9D7-106798E82E18/Article/p91>' with oldVersion = 1 and newVersion = 2
Code:
NSMutableDictionary *dict = [[data objectAtIndex:i] valueForKey:#"category_names"];
NSMutableArray *values = [[NSMutableArray alloc] init];
for (NSString *value in [dict allValues]) {
NSLog(#"value = %#", value);
[values addObject:value];
}
NSMutableSet *setElements = [[NSMutableSet alloc] init];
for (int i = 0; i < [values count]; i++) {
Category *cat = [self getCategoryFor:[values objectAtIndex:i]]; // Function which has fetch to get the category for the value "name"
[setElements addObject:cat];
}
if ([setElements count] > 0)
[article addCategories:setElements];
// Save the context.
NSError* error;
if (![managedObjectContext save:&error]) {
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
} else
NSLog(#" %#", [error userInfo]);
}
[article release];
[values release];
[setElements release];
The error your getting is not actually related to the data itself but rather caused by having two irreconcilable versions of the data model writing to the same persistent store file.
You must have created a data model, used it write some data to the persistent store and then updated the model. This is normally not an issue unless you so alter the data model that the automatic migration cannot merge the old and new data.
If this is still under development and you don't need the existing data, the easiest solution is to delete the app off the simulator and then build and run it again using only the latest data model. That will require no migration and will therefore skip the merge error.