Fetch one-to-many relationship fault still shows after adding setRelationshipKeyPathsForPrefetching - core-data

Let's say my data model has two entities: Department and Person. Departments has a to-many relationship called 'departmentToPerson' with Person. Person has a to-one relationship called 'personToDepartment' with Department.
I want to populate an array of people belonging to a selected department. To selected the department I've created a UILabel that displays a departmentName that is selected from a popup tableview. When I run the app the log shows:
personToDepartment.departmentName == (entity:
Department; id: 0x8cc1920
;
data: {
departmentName = Marketing;
departmentToPerson = "";
I've read the purpose of faults and implemented setRelationshipKeyPathsForPrefetching, but still I get the fault. I'll put in the disclaimer that I'm a newbie and may be missing something obvious. When I delete the predicate my table view populates with all personName. Here's the code:
- (NSFetchedResultsController *)fetchedResultsController {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Person" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment.departmentName = %#", selectedDepartment];
NSLog(#"predicate is: %#",predicate);
[fetchRequest setPredicate:predicate];
[fetchRequest setRelationshipKeyPathsForPrefetching:[NSArray arrayWithObjects: #"departmentToPerson", nil]];
fetchRequest.returnsObjectsAsFaults = NO;
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"personName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Again, I select a new department from the UILabel, the log displays selectedDepartment, but then states relationship fault and does not populate the table.
Thanks in advance for the help!
Updated 9JUNE
I've also found this works:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment = %#", selectedDepartment];

I'm also kind of newbie to CoreData, but it seems to me that predicate is wrong. According to Log record you're about to comparing personToDepartment.departmentName to a department Object. Predicate should look like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment.departmentName = %#", selectedDepartment.departmentName];
But there is a better way. selectedDepartment.departmentToPerson will return an NSSet with all persons belonging to this department (if relationship was set previously). But warning, if you'll try to NSLog(#"%#", selectedDepartment.departmentToPerson) probably you'll get "relationship fault", because CoreData will not do fetch until you address specific object in NSSet or enumerate through it. But for example NSLog(#"%d", selectedDepartment.departmentToPerson.count) will force CoreData to make fetch and you'll see number of persons in department.
UPDATE:
NSSet is empty probably because you are not setting relationship, when creating person's object. Inverse To-Many relationship from department to persons will be set automatically.
- (id)insertNewPerson:(NSDictionary *)personInfo inDepartment:(Department*)departmentObject
{
Person *personObject = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
personObject.name = personInfo.name;
personObject.departmentName = departmentObject.name;
...
personObject.personToDepartment = departmentObject; //this is RELATIONSHIP, if set, then you can access all persons from specific department by doing: departmentObject.departmentToPerson
}

Related

Saving Core Data related data and retrieving with NSPredicate and NSFetchedResultsController with multiple entities

I'm fairly new to Core Data and am still trying to understand accessing and filtering related data. My problem is either I'm not getting the data correctly into the managedObjectContext or I'm not pulling it out correctly. (I think the first, but I'm not sure.)
Here's my data model with two entities related one to many: (I plan to refactor once I get one level of relationship working.)
I have a SeasonsVC in which you click on a season name and the list of games for that season is supposed to appear in the GamesVC and you have the option to add or edit an existing game. This works fine at a first pass. I can add and edit games via this code in the GameDetailsVC:
-(IBAction)done:(UIBarButtonItem *)sender {
Game *game = nil;
if (self.gameToEdit != nil) {
game = self.gameToEdit;
NSLog(#"Hitting gametoedit");
} else {
game = [NSEntityDescription insertNewObjectForEntityForName:#"Game" inManagedObjectContext:self.managedObjectContext];
NSLog(#"Hitting new game");
}
game.opponent = self.opponentTextField.text;
//season.seasonDescription = self.seasonDescriptionTextView.text;
NSLog(#"Game to edit: %#", game.opponent);
//NSLog(#"Season: %#", season);
//NSLog(#"MOC in done: %#", self.managedObjectContext);
//NSLog(#"Season name: %#", season.seasonName);
[self dismissViewControllerAnimated:YES completion:nil];
}
I can then see the games in the GamesVC via the fetchedResultsController and delegate methods, but each game is associated with every season. Once I try to filter the data with a predicate so that I only see the games that were added for that season all the games disappear. Here's the code for that from the GamesVC:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Game" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSLog(#"Season name for predicate %#", self.season.seasonName);//shows correct season name
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"season.seasonName like '%#'", self.season.seasonName]];
[fetchRequest setPredicate: predicate];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"dateOfGame" ascending:NO] ;
[fetchRequest setSortDescriptors:#[sortDescriptor]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:#"Root2"];
self.fetchedResultsController = theFetchedResultsController;
self.fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Since I can log the correct season name right before the predicate statement, I think that the added games are not getting "associated" with the correct season when I put them in the MOC in the done: method shown above; otherwise, I'd expect the predicate to find them.
Can you help this rookie? Thanks.
A day later I have this sorted. I needed to make three changes to what I was doing.
First, in my prepareForSegue in my GameListVC I needed to pass the season along to the GameDetailsVC by changing the code to this. (Just added the one line: controller.season = self.season)
//pass the Season object to be edited
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
Game *gameToEdit = [self.fetchedResultsController objectAtIndexPath:indexPath];
controller.gameToEdit = gameToEdit;
controller.season = self.season;
Second, I had to put the new/edited game object into the MOC by adding these two lines to my done: method in the GameDetailsVC.
NSMutableSet *games = [self.season mutableSetValueForKey:#"games"];
[games addObject:game];
Third, I needed to simplify my predicate statement in my fetchedResultsController method in the GameListVC.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"season.seasonName like %#", self.season.seasonName];
So, to answer my own question, I was both not saving the game linked to the season and my predicate was wrong.
I hope some of this helps another rookie.

Getting stored values from NSSet which was fretched from core data as to-many

I am actually new to iPhone programming and have successfully create an app but I can't figure out how to retrieve the values stored in NSSet. I have 2 entities in core data related to each other. Users one-to-many to Scores. Users entity has firsNname and lastName attributes and Scores entity has onTime, wasAbsent, and dateIn attributes. I fetch using predicate based on firstName and lastName and then execute the fetch. The fetch is successful and I am able to get both entities in one fetch call. However I cannot get values for the values from Scores entity. Whatever I do, it always returns NSSet object. What I want to do is to retrieve the boolean value which was stored in onTime and wasAbsent attributes and feed them to UISwitch.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
self.managedObject = delegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Student"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"firstname == %# AND lastname == %#", #"James", #"Smith"];
NSError *error = nil;
self.scoresArray = [self.managedObject executeFetchRequest:fetchRequest error:&error];
if ([self.scoresArray count]) {
Student *student = [self.scoresArray objectAtIndex:0];
NSSet *score = student.scores;
NSArray *arr = [score allObjects];
NSLog(#"%#", [arr objectAtIndex:0]);
}
if I can directly access the Score entity instead of using NSSet, that would be ideal that way I can reference it using a dot notation.
Any help would be appreciated. Thanks
Use something like the following:
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
self.managedObjectContext = delegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Student"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"firstname == %# AND lastname == %#", #"James", #"Smith"];
NSError *error = nil;
NSArray *studentArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (Student *student in studentArray) {
Student *student = [self.studentArray objectAtIndex:0];
NSLog(#" student is %# %#", student.firstname, student.lastname);
for (Score *score in student.scores) {
// Do something with score
NSLog(#" score is %#", score);
}
}
Because Student has to-many relationship to Scores (and the property for it is scores), when you get the value of student.scores, it returns an unordered collection, which is NSSet in Foundation. So you just work with it like you work with NSSet and with collections in general.

core data retrieving data from sets with many to many relationships

I am new to core data and am still trying to get my head around it. I have a problem that I have spent hours on trying to solve. I just can't seem to solve it and was hoping someone could help me.
I can't post images yet so here is a link to my data model:
http://yourpcsolution.net/model.png
Each relationship has an inverse. A Game can have many Periods and a Period can have many Players. A Player can be in more than one Period.
Here is my code for populating DB:
- (void) prepareDB {
Games *game = [NSEntityDescription insertNewObjectForEntityForName:#"Games" inManagedObjectContext:self.managedObjectContext];
game.date = [NSDate date];
game.name = #"Game 1";
Periods *period = [NSEntityDescription insertNewObjectForEntityForName:#"Periods" inManagedObjectContext:self.managedObjectContext];
period.date = [NSDate date];
period.name = #"1st Period";
NSMutableSet *gameSet = [[NSMutableSet alloc] initWithObjects:period, nil];
game.periods = gameSet;
Players *player = [NSEntityDescription insertNewObjectForEntityForName:#"Players" inManagedObjectContext:self.managedObjectContext];
player.name = #"Player1";
NSMutableSet *playerSet = [[NSMutableSet alloc] initWithObjects:player, nil];
period.players = playerSet;
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
}
I think that's the correct way to add data because when looking at the sql db the data is there. My problem is in trying to retrieve the players that belong to each period. Because Periods is a set in Games and Players is a set in Periods in core data. So I can't retrieve players by going: game.periods.players
Here is my code for retrieving a Period and in the xcode log i am getting a fault:
"Relationship 'players' fault on managed object (0x8e72bc0) <Periods: 0x8e72bc0>
(entity: Periods; id: 0x8e727c0 <x-coredata://7F63902B-FCB6-4ACA-BB40-904755D37A4A/Periods/p1>;
data: {\n date = \"2013-07-09 19:35:48 +0000\";\n
games = \"0x8e6d760 <x-coredata://7F63902B-FCB6-4ACA-BB40-904755D37A4A/Games/p1>\";\n
name = \"1st Period\";\n players = \"<relationship fault: 0x9840330 'players'>\";\n})"
My Code for retrieving a Period:
NSString *firstPeriod = #"1st Period";
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name == %#", firstPeriod];
[request setEntity:[NSEntityDescription entityForName:#"Periods" inManagedObjectContext:self.managedObjectContext]];
[request setPredicate:predicate];
NSArray *period = [self.managedObjectContext executeFetchRequest:request error:&error];
I'm not sure how to proceed from here to retrieve the players. I know how to retrieve what Periods belong to each game but I don't know how to retrieve the players that belong to a period. I have been trying for days to figure this out and it's just not working.
Is my data model correct? How should I be retrieving the data? Any help or advice is appreciated.
To fetch all players of a given period, the following should work:
NSString *firstPeriod = #"1st Period";
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Players" inManagedObjectContext:self.managedObjectContext]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY periodsin.name == %#", firstPeriod];
[request setPredicate:predicate];
NSArray *players = [self.managedObjectContext executeFetchRequest:request error:&error];
"ANY" in the predicate is necessary because each player is connected to multiple Periods.
Remark: The data type of a to-many relationship is NSSet, not NSMutableSet.
ADDED: To fetch all periods for a given player, you can use the following fetch request:
Players *givenPlayer = ... ; // the given player
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Periods" inManagedObjectContext:self.managedObjectContext]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY players == %#", givenPlayer];
[request setPredicate:predicate];
NSArray *periods = [self.managedObjectContext executeFetchRequest:request error:&error];
Alternatively, you can access the relationship directly:
Players *givenPlayer = ... ; // the given player
NSArray *periods = [givenPlayer.periodsin allObjects]; // "allObjects" converts NSSet to NSArray

Can I fetch relationship's data in Core Data

Can I fetch relationship's data in Core Data?
The relationship is one-many.
For example: I have 2 entities (department and employee). The department has a to-many employees relationship and employee has a to-one department relationship. I want to get employees of department entity. Can I using fetch function to obtain the data?
Thanks in advance.
You can just use the relationship property to get the set of employees for a department:
Department *theDepartment = ...; // your department
NSSet *employeesInDepartment = theDepartment.employees; // set of Employee objects
Or, if you need an array:
NSArray *employeesInDepartment = [theDepartment.employees allObjects];
Alternatively, you can use the following fetch request:
Department *theDepartment = ...; // your department
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Employee"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"department = %#", theDepartment];
[request setPredicate:predicate];
NSError *error;
NSArray *employees = [yourManagedObjectContext executeFetchRequest:request error:&error];

NSFetchedResultsController with NSDictionaryResultsType in NSFetchRequest

I have been looking over this issue since a week and haven't got any solution, so I thought to make the question more generalized, may be it will help the users to look into it and give me a solution.
Scenario:
I have an expense tracking iOS Application and I have a view controller called "DashBoardViewController" (table view controller - with FRC) which would basically categorize my expenses/incomes for a given week, a month, or year and display it as the section header title for example : (Oct 1- Oct 7, 2012) and it shows expenses/incomes ROWS and related stuff according to that particular week or month or year.
My Question:
What I want to accomplish is :
Show Distinct Results based on "Category" attribute of "Money" entity and calculate "Sum" based on the attribute.
But, my view controller called "dashboard view controller" is filled with NSFetchedResultsController with section name key path as "type" that can be an expense or an income. In order to get Distinct results, I shall use Result type as NSDictionaryResultsType in my fetch request which will give me unique results but FRC fails, it doesn't work with that. So, how will I get my section names then? I have posted the code below.
EDIT - BASED ON MARTIN'S SUGGESTION
- (void)userDidSelectStartDate:(NSDate *)startDate andEndDate:(NSDate *)endDate
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Money" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicateDate = [NSPredicate predicateWithFormat:#"(date >= %#) AND (date <= %#)", startDate, endDate];
[fetchRequest setPredicate:predicateDate];
// Edit the sort key as appropriate.
typeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"type" ascending:YES];
dateSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
if(self.choiceSegmentedControl.selectedIndex == 0)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"cat" ascending:NO];
}
if(self.choiceSegmentedControl.selectedIndex == 1)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"vendor" ascending:NO];
}
if(self.choiceSegmentedControl.selectedIndex == 2)
{
choiceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"paidBy" ascending:NO];
}
NSArray * descriptors = [NSArray arrayWithObjects:typeSortDescriptor, dateSortDescriptor,choiceSortDescriptor, nil];
[fetchRequest setSortDescriptors:descriptors];
[fetchRequest setIncludesSubentities:YES];
NSError * anyError = nil;
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:
[entity.propertiesByName objectForKey:#"cat"],
nil];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setPropertiesToFetch:propertiesToFetch];
NSArray * objects = [context executeFetchRequest:fetchRequest error:&anyError];
for (NSDictionary *d in objects)
{
NSLog(#"NSDictionary = %#", d);
}
NSSet *uniqueSet = [NSSet setWithArray:[objects valueForKey:#"cat"]];
uniqueArray = [NSMutableArray arrayWithArray:[uniqueSet allObjects]];
self.categoryArray = uniqueArray;
if(_fetchedResultsController)
{
[_fetchedResultsController release]; _fetchedResultsController = nil;
}
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:#"type" cacheName:nil];
//self.fetchedResultsController.delegate = self; in order to stop "change tracking"
if(![_fetchedResultsController performFetch:&anyError])
{
NSLog(#"error fetching:%#", anyError);
}
[fetchRequest release];
//Finally you tell the tableView to reload it's data, it will then ask your NEW FRC for the new data
[self.dashBoardTblView reloadData];
self.startDate = startDate;
self.endDate = endDate;
}
With this code, SECTION NAME KEY PATH is not working and it's complaining that the object will be placed in unnamed section.
A fetched results controller does not support change tracking (i.e. the FRC delegate is set) in combination with a fetch request with NSDictionaryResultType.
The reason is documented with the setIncludesPendingChanges: function:
Special Considerations
A value of YES is not supported in conjunction
with the result type NSDictionaryResultType, including calculation of
aggregate results (such as max and min). For dictionaries, the array
returned from the fetch reflects the current state in the persistent
store, and does not take into account any pending changes, insertions,
or deletions in the context.
Change tracking of the FRC implies includesPendingChanges = YES for the fetch request, and that does not work with NSDictionaryResultType.
One workaround could be to use a FRC without change tracking, so you do not set the FRC delegate. But that means that to update your table view, you have to
save the managed object context, and
call performFetch on the FRC and reloadData on the table view.
Another workaround could be to use the FRC to fetch all sections and rows without the sum aggregation, and use the results to compute new table rows with the aggregation in memory (e.g. in controllerDidChangeContent).
UPDATE: (From the discussion) Another important point is that if you use a fetch request with NSDictionaryResultType, then the sectionNameKeyPath of the fetched results controller must be included in the propertiesToFetch of the fetch request.

Resources