Grouping three entities with many-to-many relationships - iphone

During the last few days I've been trying to figure out how to achieve this and there is no way with my little knowledge.
I've designed three entities List <<-->> Item <<-->> Store in core data model designer. Each of them with only one attribute called "name".
The goal is to select a List, then show up all items within the list grouped by Store.
I've tried to use:
// Set entity.
entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set object filters.
predicate = [NSPredicate predicateWithFormat:#"ANY list.name == %#", self.list.name];
[fetchRequest setPredicate:predicate];
// Set FRC
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"store.name" cacheName:nil];
Error:
'Invalid to many relationship in setPropertiesToFetch: (store.name)'
and this way too for populating rows by-hand (I don't know how yet):
// Set entity.
entity = [NSEntityDescription entityForName:#"Store" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set object filters.
predicate = [NSPredicate predicateWithFormat:#"SUBQUERY(item.list, $x, $x.name == %#).#count > 0", self.list.name];
[fetchRequest setPredicate:predicate];
// Set FRC
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"name" cacheName:nil];
Error:
'Only allowed one toMany/manyToMany relationship in subquery expression collection (SUBQUERY(item.list, $x, $x.name == "List02"))'
And also tried Fetched Properties and other ways that reach no-place.
Any ideas?
Regards.
Pedro.

Ok, here is the solution (danypata & Martin R give me the key).
To achieve this you should add a new entity in order to break the many-to-many relationship. The final Core Data model is: List <<-->> Item <-->> ItemStore <<--> Store. "ItemStore" entity doesn't need to have any attribute, just relationships.
The code...
- (NSFetchedResultsController *)fetchedResultsController
{
[...]
// Set entity.
entity = [NSEntityDescription entityForName:#"ItemStore" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set object filters.
predicate = [NSPredicate predicateWithFormat:#"ANY item.list.name == %#", self.list.name];
[fetchRequest setPredicate:predicate];
// Edit the sort key as appropriate.
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"item.name" ascending:YES];
sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Set FRC.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"store.name" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[...]
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Item *item;
ItemStore *itemStore;
[...]
itemStore = (ItemStore *)[self.fetchedResultsController objectAtIndexPath:indexPath];
item = itemStore.item;
cell.textLabel.text = item.name;
[...]
}
Final notes:
Break many-to-many relationship with a new entity (ItemStore) and two one-to-many relationships (Item <-->> ItemStore & ItemStore <<--> Store).
Due to Item's "store" relationship is optional (in my model), items without Store are not retrieved, if you want to do so you should assign them to a "No Store" object by default.
Thanks all.
Pedro.

As has been pointed out, you cannot fetch an items and group by store if one item can be in more than one store. However, using a NSFetchedResultsController for in display in a table view, or something similar, this is still possible (and quite usual).
Simply fetch the entity you want to group by, in this case Store. Adjust your table view datasource methods accordingly:
Number of sections:
return _fetchedResultsController.fetchedObjects.count
Title for section:
Store *store = _fetchedResultsController.fetchedObjects[indexPath.section];
return store.name;
Number of rows in section:
Store *store = _fetchedResultsController.fetchedObjects[indexPath.section];
return store.items.count;
One item in a store:
Store *store = _fetchedResultsController.fetchedObjects[indexPath.section];
Item *item = [[store.items sortedArrayUsingSortDescriptors:#[
[NSSortDescriptor sortDescriptorWithKey:#"sortAttribute" ascending:YES]]
objectAtIndex:indexPath.row];
// configure cell with information from item.
So while this is possible, perhaps you still want to rethink your data model. Does it really make sense that one item is in more than one Store? If it were not, you could use the plain vanilla fetched results controller.

Related

How can I use NSFetchedResultsController to fetch results based on an entity's relationship?

I have 2 views that contain a UITableView each. They are both displayed at the same time, side by side, on an iPad.
I am using Core Data for all data. Both tables need to be edited (rows added, deleted, etc), so I'd like to use a NSFetchedResultsController in each view to handle all this for me.
The contents of the second table depend on what is selected in the first table. So, when selecting an item in the first table, that object is passed to the view with the second table (so I do already have access to the data that should go into the second table), but I'd like to try to use all the built-in handling of the NSFRC if possible.
The model is along the lines of: University (uniID, uniName, students) and Student (stuID, stuName, university). So the relationship is: University <-->> Student.
I'm using the following code in the NSFRC, but it's returning 0 results:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSManagedObjectContext *context = appDelegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Student" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]initWithKey:#"stuName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"university == %#",self.selectedUniversity];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
fetchedResultsController.delegate = self;
[sort release];
[fetchRequest release];
[theFetchedResultsController release];
return fetchedResultsController;
}
I would be most grateful if someone could at least point me in the right direction...
Have you remembered to performFetch: ?
i.e.
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
Ok, so I solved the issue. The NSFRC wasn't being updated when the predicate term needed to change (i.e. a University had been selected), of course, because of:
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
Having the NSFRC recreated every time it's called (by removing the above code) doesn't work either, because the fetch needs to be executed after it's been created, which can't happen just before numberOfRowsInSection is called (as this method calls, and therefore recreates, the NSFRC).
So, I added a BOOL to the view called newFetchRequired which is set to YES every time a new University is selected. In the NSFRC, the above code should be changed to:
if (fetchedResultsController != nil && !newFetchRequired) {
return fetchedResultsController;
}
newFetchRequired = NO;
The fetch is then performed correctly (which calls and recreates the NSFRC):
[self.fetchedResultsController performFetch:&error];
I hope this helps anyone in a similar situation.
Thanks to Ashley for the alternative suggestion.
I don't know if you're still checking this and I'm not quite sure if I understood your question correctly.. but why do you create the NSFRC from scratch when you just want to change the predicate? Here's how I would do it:
When a new University is selected just add in the code right there:
NSFetchRequest *fetchRequest = [self.fetchedResultsController fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"university == %#",self.selectedUniversity];
[fetchRequest setPredicate:predicate];
NSError *error;
[fetchedResultsController performFetch:&error];
and you're done. (Sorry if this is totally not what you were looking for).
edit: You could also simply write this into a private function and call it every time a university is selected.

Core Data: Fetch all entities in a to-many-relationship of a particular object?

in my iPhone application I am using simple Core Data Model with two entities (Item and Property):
Item
name
properties
Property
name
value
item
Item has one attribute (name) and one one-to-many-relationship (properties). Its inverse relationship is item. Property has two attributes the according inverse relationship.
Now I want to show my data in table views on two levels. The first one lists all items; when one row is selected, a new UITableViewController is pushed onto my UINavigationController's stack. The new UITableView is supposed to show all properties (i.e. their names) of the selected item.
To achieve this, I use a NSFetchedResultsController stored in an instance variable. On the first level, everything works fine when setting up the NSFetchedResultsController like this:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all item objects.
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetch setFetchBatchSize:10];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
However, on the second-level UITableView, I seem to do something wrong. I implemented the fetchedresultsController in a similar way:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all property objects that belong to the previously selected item
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
// fetch all Property entities.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Property" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
// limit to those entities that belong to the particular item
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"item.name like '%#'",self.item.name]];
[fetch setPredicate:predicate];
// sort it. Boring.
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
NSError *error = nil;
NSLog(#"%d entities found.",[self.moContext countForFetchRequest:fetch error:&error]);
// logs "3 entities found."; I added those properties before. See below for my saving "problem".
if (error) NSLog("%#",error);
// no error, thus nothing logged.
[fetch setFetchBatchSize:20];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
Now it's getting weird. The above NSLog statement returns me the correct number of properties for the selected item. However, the UITableViewDelegate method tells me that there are no properties:
-(NSInteger) tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"Found %d properties for item \"%#\". Should have found %d.",[sectionInfo numberOfObjects], self.item.name, [self.item.properties count]);
// logs "Found 0 properties for item "item". Should have found 3."
return [sectionInfo numberOfObjects];
}
The same implementation works fine on the first level.
It's getting even weirder. I implemented some kind of UI to add properties. I create a new Property instance via Property *p = [NSEntityDescription insertNewObjectForEntityForName:#"Property" inManagedObjectContext:self.moContext];, set up the relationships and call [self.moContext save:&error]. This seems to work, as error is still nil and the object gets saved (I can see the number of properties when logging the Item instance, see above). However, the delegate methods are not fired. This seems to me due to the possibly messed up fetchRequest(Controller).
Any ideas? Did I mess up the second fetch request? Is this the right way to fetch all entities in a to-many-relationship for a particular instance at all?
You need to actually perform the fetch for the table view controller:
// ...create the fetch results controller...
NSError *fetchRequestError;
BOOL success = [fetchedResultsController performFetch:&fetchRequestError];

Add a UISegmentedController to navigation bar with NSFetchedResultsController

I am working with a UITableView that gets its data from an NSFetchedResultsController. I would like to add a UISegmentedControl to my navigation bar that would toggle the table between displaying all of the records and only the records where starred == YES.
I have read some other SO posts indicating that one way to do this is to create a second NSFetchedResultsController that has an NSPredicate with starred == YES, but it seems awfully overkill to create a second NSFetchedResultsController.
Is there a simpler way to do this?
Not according to the docs on NSFetchedResultsController. If you take a look at the documentation on the fetchRequests property, you'll see the following note:
Important: You must not modify the
fetch request. For example, you must
not change its predicate or the sort
orderings.
Since the fetchRequest property is read-only, the only option is creating a new fetched results controller.
It might be possible to change the predicate and have things work, but it's generally a bad idea to do stuff that goes explicitly against things Apple says in the documentation, because it could break in a future release.
And, beware of premature optimization! Unless you've tested it out and found out that creating a whole new fetched results controller is a big performance drain, it's not worth trying to do something in a non-recommended way.
Here's how I set a new predicate on my fetched results controller. fetchedResultsController is a property of my view controller. predicate is a private ivar of the view controller.
I already had all of the code for creating the fetched results controller on demand, so to set the predicate it's just a matter of deleted the cached one.
- (void)setPredicate:(NSPredicate *)newPredicate {
predicate = [newPredicate copy];
// Make sure to delete the cache
// (using the name from when you created the fetched results controller)
[NSFetchedResultsController deleteCacheWithName:#"Root"];
// Delete the old fetched results controller
self.fetchedResultsController = nil;
// TODO: Handle error!
// This will cause the fetched results controller to be created
// with the new predicate
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
}
This code if based on the boilerplate XCode generates when you start a project that uses Core Data.
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:[MyEntity entityName] inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:predicate];
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}

Core Data Relationships, NSPredicates and the NSFetchedResultsController

This has been driving me nuts all day.
I have a weird bug that I think I have narrowed down to an NSPredicate. I have two entities: List and Person. List has a to-many relationship to Person called persons and Person has a to-many relationship to List called lists.
I pass to my a tableview controller a List object. I then want that tableview controller to display the Persons that belong to that list object. I am doing this with a NSFetchedResultsController.
When setting up the NSFRC, I have the following code (memory management omitted for clarity). The List in question is myList:
// Create the request and set it's entity
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create a predicate to get the persons that belong to this list
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(ANY lists == %#)", myList];
// Assign this predicate to the fetch request
[fetchRequest setPredicate:predicate];
// Define some descriptors
NSSortDescriptor *locationDescriptor = [[NSSortDescriptor alloc] initWithKey:#"location" ascending:YES];
NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:locationDescriptor, lastNameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"location" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
I think the problem is with this line (because it disappears if I remove it):
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(ANY lists == %#)", myList];
What is happening is when the parent view passes myList to the tableview controller, the simulator just hangs. No crash log in the console or anything. It's almost as if it's just taking AGES to sort out the NSFRC.
Is this a problem with the predicate I'm using?
Do you you need to use NSFetchedResultsController when you can obtain the Persons from the list passed into the tableViewController?
NSSet *people = myList.persons;
You are correct, you can just use myList.persons, an NSFetchedResultsController is not necessary in this situation.
Thanks for the suggestions re: using an NSSet. After hours of bug-tracking I realised that the problem lie in my cellForIndexPath method of the table view (so, unrelated to the NSFRC).

Group of sections not consistent when using NSFetchedResultsController

I am working with a NSFetchedResultsController whose fetchRequest has a predicate. However, it seems that the query doesn't give me consistent groupings each time I execute it.
I've set the 'sectionNameKeyPath' for the NSFetchedResultsController and I get a different number of sections returned based on whether I have been working with the root object immediately prior to running the fetch. Sometimes I get 3 sections and other times, it returns 1 section, the expected result.
How I am creating the FetchRequestController:
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Configure the request's entity and its predicate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// The predicate to find all employees associated with a Group
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY SELF.groups IN %#",
[division groups]];
[fetchRequest setPredicate:predicate];
// Sort based on create date and time
NSSortDescriptor *createDateSortDcptor = [[NSSortDescriptor alloc] initWithKey:#"createDateTime" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:createDateSortDcptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// should be grouped by the 'Group' employee belongs to.
NSFetchedResultsController *controller =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"groups"
cacheName:#"Root"];
My object model is the same that was outlined in this other question:
https://stackoverflow.com/questions/1580236/how-to-setup-a-predicate-for-this-query
Is there a way to make sure I am getting consistent grouping each time?
It turns out that it's simple as doing:
NSFetchedResultsController *controller =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"groups.name"
cacheName:#"Root"];
I didn't realize I could append 2nd level property names within the 'sectionNameKeyPath'