This question already has answers here:
Programmatically Update an attribute in Core Data
(5 answers)
Closed 9 years ago.
I know that this has been answered many times but I can't seem to get mine to work.
I have an entity called SchoolNames and this has a single attribute called nameOfSchool. I want to be able to update this attribute.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"SchoolName"];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"NameOfSchool" ascending:YES]];
NSError *errorLoading = nil;
self.schoolNames = [context executeFetchRequest:fetchRequest error:&errorLoading];
NSLog(#"%d", self.schoolNames.count );
I can see in the NSLog that I am loading my data into the schoolNames array. But I have no idea how to update an entry in the array and then save that back to the entity.
Any help much appreciated.
The return value of executeFetchRequest is an array of SchoolName objects. To update these objects, you just assign a new value to the attribute:
for (SchoolName *sn in self.schoolNames) {
sn.nameOfSchool = ... ; // new value;
}
NSError *error;
if (![context save:&error]) {
// Save failed, handle error ...
}
Martin's suggestion is really valid. I upvoted ;).
The other way is to access the attribute you are interested in through KVC (if you have not subclassed the entity SchoolName).
for (NSManagedObject *schoolObject in [self schoolNames]) {
[schoolObject setValue:#"yourNewValue" forKey:#"nameOfSchool"];
}
Also remember to save, on the contrary changes will be only on memory (not in the store).
P.S. In the code I suppose that nameOfSchool is of type NSString. Other values are possible.
Related
Setup: I have a collection of parent objects, call them ObjectA. Each ObjectA has a one-to-many relation to ObjectB. So, one ObjectA may contain 0..n ObjectB-s, and each ObjectB has a specific ObjectA as its parent.
Now, I would like to do a Core Data fetch of ObjectA-s, where they are sorted by their latest ObjectB. Is it possible to create a sort descriptor for that?
There is a related question that describes exactly the same situation. The answer suggests denormalizing the attribute from ObjectB into ObjectA. This would be OK if there really is no way to do this with one fetch request.
The related question also mentions:
Actually, I just had an idea! Maybe I can sort Conversations by messages.#max.sortedDate…
I tried. It doesn’t seem to be possible. I get this error:
2012-10-05 17:51:42.813 xxx[6398:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Keypath containing
KVC aggregate where there shouldn't be one; failed to handle
ObjectB.#max.creationTime'
Is denormalizing the attribute into ObjectA the only/best solution?
You could add an attribute in ObjectB which is the time stamp of the add date, then in the fetch request you can do something like this:
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:#"objectB.addTime" ascending:YES];
...
fetchRequest.sortDescriptors = #[descriptor];
I know this question is a bit old but what I did was get all ObjectBs, iterate over the results and pull out the ObjectB property and add it to a new array.
NSFetchRequest *fetchRequest = [NSFetchRequest new];
[fetchRequest setEntity:self.entityDescForObjectB];
// sort
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
NSError *error = nil;
NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(#"Error fetching objects: %#", error.localizedDescription);
return;
}
// pull out all the ObjectA objects
NSMutableArray *tmp = [#[] mutableCopy];
for (ObjectB *obj in fetchedObjects) {
if ([tmp containsObject:obj.objectA]) {
continue;
}
[tmp addObject:obj.objectA];
}
This works because CoreData is an object graph so you can work backwards. The loop at the end basically checks to see if the tmp array already has a specific ObjectA instance and if not adds it to the array.
It's important that you sort the ObjectBs otherwise this exercise is pointless.
I have an entity called Practice and I use a View Controller called SelectorViewController to select one of the practices, selectedPractice. I then return selectedPractice to a view Controller called RegularViewController where I display some of the selectedPractice attributes. All of this works fine. However the app has a number of other View Controllers which can be reached by modal segues from instances of RegularViewController. As a result, if I leave and then come back to RegularViewController, selectedPractice is reset as null. I would also like to save selectedPractice so that it is available at app initialisation if it has previously been set in SelectorViewController. How do I achieve this by making selectedPractice persistent across the app, and available at runtime?
Regards
Thanks to the post above, which was great, I managed to sort it. Here is my code, which may be very clumsy, but it works.
Firstly, as I loaded the fetchedObjects into a PickerView in SelectorView Controller, I set an attribute "isSelectedPractice" to "NO" with the following code:
for (Practice *fetchedPractice in [self.fetchedResultsController fetchedObjects]) {
[fetchedPractice setValue:#"NO" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
I then identified for the selected Practice:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
Practice *practice = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
self.selectedPractice = practice;
NSLog(#"The '%#' practice was selected using the picker", self.selectedPractice.name);
}
as the view Segue'd back to RegularViewController I set the isSelectedPractice attribute for selectedPractice to YES. I kept it this late as I didn't want more than one selection in the PickerView to result in multiple objects with isSelectedPractice YES.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SavedPractice Segue"])
{
[self.selectedPractice setValue:#"YES" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
NSLog(#"Setting SelectedPractice as '%#' in RegularViewController with isSelectedPractice as '%#'",self.selectedPractice.name,self.selectedPractice.isSelectedPractice );
RegularViewController *rvc= segue.destinationViewController;
rvc.delegate = self;
rvc.selectedPractice = self.selectedPractice;
}
else {
NSLog(#"Unidentified Segue Attempted!");
}
}
I then set the following Predicate in the setupFetchedResultsController method of RegularViewController:
request.predicate = [NSPredicate predicateWithFormat:#"isSelectedPractice = %#", #"YES"];
Many thanks for the help
Without seeing your actual project, one way I know will work but might be a little too round a bout would be to add an attribute "isSelectedPractice" to your entity. You could make it a BOOL, but I've had mixed results with BOOL's in Core Data, I prefer to just leave it as a NSString and set it to "yes" or "no". Then when you pull it down, modify it or add it to core Data as a entity with isSelectedPractice set to "yes". Then in your other controllers, do a
if (self.managedObjectContext == nil) {
self.managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
then do a fetch request to get entities with a predicate which is looking for isSelectedPractice equaling "yes". If you need actual code samples on how to do this let me know and I'll edit them in.
I have created few entities in context for saving it in db using
AppCalendarEntity *appCalendar = [AppCalendarEntity getInstanceWithManagedDocument:manageDocument];
After adding a few entities I execute flowing fetch request
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"AppCalendarEntity"];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
It returns me the result including only the entities I have added in context using first command and NOT the entries already saved in database. I have made sure that at this stage the document state is UIDocumentStateNormal.
When I add this line to already open document (UIDocumentStateNormal) it returns me the expected result, i.e. it fetch results from db as well as memory context which has not yet been saved to db.
[managedDocument openWithCompletionHandler:^(BOOL success)
{
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"AppCalendarEntity"];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
}
My question is
1- I expect that the result of query should be the same in both cases. Why it is not so in the above case.
2- To me if document state is UIDocumentStateNormal I should not be calling "openWithCompletionHandler" in context to open the document. In this particular scenario what difference it is making in NSFetchRequest which gives me the desired result after adding this.
Please let me know if I'm getting wrong
Here is the complete code
This is the complete code of the function
+ (void ) saveCalendarArrayInDbIfItAlreadyDoesNotExist : (NSArray*) appCalendarArray managedDocument: (UIManagedDocument*) managedDocument completionBlock : ( void(^) (NSArray* ObjectSavedSuccesfully, NSError *InternalError)) handler
{
// i dont know why i have to do it :( if i dont add openWithCompletionHandler my query doesnt fetch result from db rather just do query in-memory context and not db
[managedDocument openWithCompletionHandler:^(BOOL success)
{
void (^completionHandler)(NSArray* , NSError* );
completionHandler = [handler copy ];
NSError *error = nil;
NSMutableArray *array = [[NSMutableArray alloc] init];
for (id appCalendar in appCalendarArray) {
if([appCalendar isKindOfClass:[AppCalendarEntity class]])
{
AppCalendarEntity *appCalendarEntity = (AppCalendarEntity*) appCalendar;
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"MyEntity"];
requestToSeeIfCalendarWithIdExist.predicate = [NSPredicate predicateWithFormat:#"identifier = %#", appCalendarEntity.identifier];
NSError *InternalError = nil;
[requestToSeeIfCalendarWithIdExist setShouldRefreshRefetchedObjects:YES];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError] ;
// "result" is different when we encapsulate it in openWithCompletionHandler and when we don't…….MY PROBLEM
if(result == nil)
{
// return error
}
// 1 object always return that depict the in memory(context) object we created but not saved. I expect it should be zero because no object has yet been saved to database..
else if(result.count > 1)
{
[managedDocument.managedObjectContext deleteObject:appCalendar];
}
else
{
[array addObject:appCalendarEntity];
}
}
else
{
// error handling
}
}
if (error != nil)
{
completionHandler (nil, error);
return;
}
// saving all the objects
[ managedDocument updateChangeCount:UIDocumentChangeDone ];
}
When using UIManagedDocument, you do not call save on the MOC because it implements auto-save. however, it needs to be told that an auto-save should take place at some point in the future.
Get rid of that call to openWithCompletionHandler in that function (I know it was just there for purposes of debugging this problem).
Replace
[managedDocument.managedObjectContext save:&InternalError ]
with
[managedDocument updateChangeCount:UIDocumentChangeDone];
This will notify the document that it can now be saved.
EDIT
First, I think you should get rid of the debugging hacks. You can add NSLog or NSAssert, but the rest of that stuff just makes it hard to tell why you want, and confuses the real issue.
Second, what is your real goal here? I can see the name of the method, and I can see the code, but they do not match.
There is so much "cruft" here, it is hard to understand your problem. I am going to repost your code, along with an edit to remove the "open" stuff, and annotate it with questions as code comments.
Hopefully, this change will help you solve your problem.
// First, the method name seems to indicate that some objects will be added
// to the database. however, the only database work in this method is removal.
// I don't get it.
+ (void ) saveCalendarArrayInDbIfItAlreadyDoesNotExist : (NSArray*) appCalendarArray managedDocument: (UIManagedDocument*) managedDocument
{
NSError *error = nil;
NSMutableArray *array = [[NSMutableArray alloc] init];
for (id appCalendar in appCalendarArray) {
if([appCalendar isKindOfClass:[AppCalendarEntity class]]) {
// OK, we are filtering the array of objects. We are only interested in
// objects of type AppCalendarEntity, and are going to use its identity
// property to look for objects of type MyEntity.
// What is the relationship between AppCalendarEntity and MyEntity?
AppCalendarEntity *appCalendarEntity = (AppCalendarEntity*) appCalendar;
NSFetchRequest *requestToSeeIfCalendarWithIdExist = [NSFetchRequest fetchRequestWithEntityName:#"MyEntity"];
requestToSeeIfCalendarWithIdExist.predicate = [NSPredicate predicateWithFormat:#"identifier = %#", appCalendarEntity.identifier];
NSError *InternalError = nil;
[requestToSeeIfCalendarWithIdExist setShouldRefreshRefetchedObjects:YES];
NSArray *result = [managedDocument.managedObjectContext executeFetchRequest:requestToSeeIfCalendarWithIdExist error:&InternalError];
// OK, now we just got a result from searching for a MyEntity, where
// its identifier is the same as the appCalendarEntity.
if(result == nil)
{
// return error
}
// 1 object always return that depict the in memory(context) object we created but not saved. I expect it should be zero because no object has yet been saved to database..
else if(result.count > 1)
{
// I am extremely confused by this code. First, why are you
// checking for more than 1 object? The method name indicates
// you are going to insert something. Furthermore, you are only
// deleting one object. How many do you expect? Also, why are
// you deleting an appCalendar? You were searching for a MyEntity.
// If an appCalendar is a MyEntity, then that's terrible naming.
// Furthermore, it would explain why you are finding it...
// because you create entities by inserting them in a MOC to
// begin with!
[managedDocument.managedObjectContext deleteObject:appCalendar];
}
else
{
// Even more confusion. You are adding this object to an internal
// array, not the database. Furthermore, you are doing it if there
// are either 0 or 1 MyEntity objects in the database with matching
// identifier.
[array addObject:appCalendarEntity];
}
}
}
// saving all the objects
// OK - but the only thing being saved are the ones you deleted...
[ managedDocument updateChangeCount:UIDocumentChangeDone ];
}
Finally, if my hunch is correct, and the calendar objects are actually MyEntity objects, they are already in the MOC - because that's how they get created. When you do a fetch, you can force the search to ignore pending changes (as noted in one of my previous comments) and only accept saved changes.
If you want to ignore pending changes,
fetchRequest.includesPendingChanges = NO;
#Jody Problem has been resolved and thank you for giving time to this question.
First let me address your confusions
1- Yes function is intended to save in the database and it is a helping function. The parameter "appCalendarArray" being passed to this function consist of entities that has already been created in context. I intentionally eliminated the logic since it involves communicating with external apis, parsing json etc etc. The code required for inserting entities in context has already been included in first part of the question.
AppCalendarEntity *appCalendar = [AppCalendarEntity getInstanceWithManagedDocument:manageDocument];
2- I eliminate the entities from context which has been constructed but not yet saved from context, based upon a column in database that should be unique. If we have identifier for object already in database, we do not want to resave it. So, I simply delete it from context. This function works as expected, entities are not re-saved in database. The last line do save the objects that are left in context if any. Most of the time there are a lot.
3- Sorry for mistyping AppCalendarEntity and MyEntity are the same.
Solution
I have added this flag fetchRequest.includesPendingChanges = NO; , delete db, restarted Xcode and it started working. Thank you for your persistence
I have an NSPersistentDocument subclass using NSManagedObject subclasses for my data.
When a new document is opened, I do some initializing of data structures (trivial amount of populating fields). What I've noticed is that the Untitled document gets autosaved, and when the application re-opens, that document gets loaded. If the application quits, the user doesn't (by default) get prompted with the save dialog. If the window closes, the user does.
First question:
I want to call up the save dialog when the user quits the application. I don't want this Untitled document hanging around (under normal circumstances). I either want it saved or trashed.
I attempted to fill out:
- (void)applicationWillTerminate:(NSNotification *)aNotification
In order to trigger the document to be saved. Calling save: on the context at this point gives an error. From what I can tell, this is because the user hasn't yet saved the file on their own. In addition, calling [self close]; or [[self windowForSheet] close]; close the window without saving.
How can I force the save dialog to come up? How can I trash the untitled document?
Second question (no, I can't count):
Since when the application starts, there may or may not be an Untitled document to deal with, I'm trying to keep track of the state in another model. I've already found that the initial data (to which I referred earlier) is present when the Untitled document came up. My other model has some metadata, including a success flag/state for the populated data. Once the populated data is all in place and correct, the state indicates as such. Unfortunately, while my populated data is being loaded when the app starts with a pre-existing Untitled document, the metadata class is not.
Please excuse the roughness of the code, at this point, I'm mucking it up until I can see that it's working how I want before I polish it back off:
- (bool) createGameState {
NSEntityDescription* description = [NSEntityDescription entityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:description];
NSError *error = nil;
NSArray *array = [[self managedObjectContext] executeFetchRequest:req error:&error];
[req release];
req = nil;
GameState* result = nil;
if (array) {
NSUInteger count = [array count];
if (!count) {
// Create the new GameState.
DebugLog(#"Creating GameState");
result = [NSEntityDescription insertNewObjectForEntityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
[result setIsLoaded:[NSNumber numberWithBool:NO]];
} else {
if (count > 1) {
NSLog(#"WARNING: Potentially Corrupt Game State. found: %lu", count);
}
result = [array objectAtIndex:0];
if ([result isLoaded]) {
[self variantLoaded];
} else {
// In this case, we have an aborted set-up. Since the game isn't
// playable, just refuse to create the GameState. This will
// force the user to create a new game.
return NO;
}
}
} else {
DebugLog(#"error: %#", error);
}
[game setState:result];
return result;
}
Note that array is always present, and count is always zero. No, I'm not explicitly calling save: anywhere. I'm relying on the standard auto-save, or the user performing a save.
EDIT:
I installed the Core Data Editor app. It turns out the issue isn't on saving the data, but on loading it. (Note: Due to another issue, the app saves as binary when instructed to save as XML, which causes much head banging.)
I've broken it down to the simplest code, which should pick up all objects of type GameState in an array. It retrieves none, despite there clearly being objects of the appropriate type in the saved file:
NSManagedObjectContext* moc = [self managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:#"GameState" inManagedObjectContext:moc];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:entity];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:req error:&error];
Array is not null, but [array count] is 0.
At this point, I'm guessing it's something simple that I'm overlooking.
Second EDIT:
I added -com.apple.CoreData.SQLDebug 5 and saved as SQLite. The call to executeFetchRequest does not generate any debug logs. I do see the INSERT INTO ZGAMESTATE entry show up in the logs. It seems that executeFetchRequest is not getting passed to the backend.
Third EDIT (this one burns):
I created a new xcode project, using core data (as I had with the other). I copied just this one function (stubbing where necessary) and plopped a call to it in windowControllerDidLoadNib. In this new project, the code above works.
Found the problem.
I errantly was loading objects in Document's - (id) init call. Moved to windowControllerDidLoadNib (which is what I did in the test version) and it worked fine.
I am wanting to set up a basic relationship with two entities in Core Data, but the relationship is either not saving, or is not working properly and I'm not sure why.
The two entities are Character and Avatar, its a one-to-one relationship. A character can have 1 avatar. Technically, it should be a "one avatar can be owned by many characters", but I'll deal with that later.
I want to add characters and assign them an avatar.
There are already 10 avatars in Core Data and 1 character, both of which I've verified via the Terminal and SQLite.
The problem is, I'm having troubling "finding an avatar by a name and then saving the relationship to a character".
So far,
I set up a fetch request called: "frqAvatarWithName" where the Predicate has the following structure:
[quote]
name == $AVATAR_NAME
[/quote]
This is so: I can find an avatar with a certain name; and then I can create a relationship with a character.
Issue 1: It gets to execute the query but then never displays how many records there are.
I get a EXC_BAD_ACCESS error in debug mode and I have traced it back to the fetch request template handling -- so, this must be in error or I have done it wrong.
Issue 2: I am not sure if I am even setting up this "basic" relationship up properly.
[code]
// This code is meant to find an avatar with a certain name and then save the relationship
// between a character and said avatar.
// This is my app delegate file for the moment
// All the files are present, and I have deleted/recreated the app various times
-(void)characterMaker
{
NSLog(#"Inside characterMaker...");
NSError *error = nil;
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObjectModel *model = [self managedObjectModel];
// Find an avatar with a specific name
NSString *nameToFind = #"avt_player_1";
// Use a Fetch request template
NSDictionary *subs = [NSDictionary dictionaryWithObjectsAndKeys:nameToFind, #"AVATAR_NAME", nil];
NSFetchRequest *fetchRequest = [model fetchRequestFromTemplateWithName:#"frqAvatarWithName"
substitutionVariables:subs];
// Set the entity to use
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Avatar"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Execute the query (it never even reaches this point)
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle the error
NSLog(#"Error -- %#", [error localizedDescription]);
abort();
}
NSLog(#"Found %# records", [fetchedObjects count]);
// Print out avatar names
for (Avatar *a in fetchedObjects)
{
NSLog(#"Name = %#", [a valueForKey:#"name"]);
}
// This is where I would use `a` and store it in a character entity, and thus create the relationship
[/code]
I gave up on this and did the whole project with the FMDatabase project and SQLite; I've been able to resolve the problem this way.
Thread closed.