Core Data Relationships, NSPredicates and the NSFetchedResultsController - iphone

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).

Related

Grouping three entities with many-to-many relationships

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.

Core Data to-many relationship get data

In my code i want to create tableView with List sections. I use scheme like this one:
I use NSFetchResultController which i define in this way:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Item" inManagedObjectContext:coreDataController.masterManagedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"addedAt" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"itemIsChecked = 1"];
[fetchRequest setPredicate:predicate];
[fetchRequest setResultType:NSDictionaryResultType];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:coreDataController.masterManagedObjectContext sectionNameKeyPath:#"toList.listName"
cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Now in cellForRowAtIndexPath: i want to get data form my fetchResultController, so i do this in way:
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
and then if i want to access one of the item's field (for example itemText), it crash:
NSLog(#"item itemtext = %#", item.itemText);
with error:
-[NSKnownKeysDictionary1 itemText]: unrecognized selector sent to instance 0x1215fd90
What i do wrong in my code?
You have set
[fetchRequest setResultType:NSDictionaryResultType];
and therefore the fetched results controller returns NSDictionary objects, not Item objects. So your element
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
is a NSDictionary, not an Item. Since dictionaries do not have a itemText method, item.itemText crashes. You could retrieve the value from the dictionary with
NSDictionary *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"item itemtext = %#", [item objectForKey:#"itemText"]);
But if you don't have a specific reason to set the result type to NSDictionaryResultType, you should just delete that line. Change tracking of the fetched results controller (i.e. automatic table view updates) do not work with resultType == NSDictionaryResultType.
Note also that if you have set a sectionNameKeyPath, then you must add a sort descriptor with the same key path "toList.listName" and use it as first sort descriptor for the fetch request.
unrecognized selector sent to instance generally occurs due to bad memory management. Check if you are trying to point an object which was released earlier. Also check for IBOutlet connection in xib for lable itemText.

Dynamically break UITableView into arbitrary sections?

I have a core data application which displays a list of custom objects, each representing theatrical productions.
Each show object has a bunch of properties - name, logo, opening date, show type.
I can retrieve them fine, and sort by type just fine - but I would like each show type to be its own section in the table.
I don't know ahead of time how many show types will be represented in the result set, so I don't know initially what the numberofsections should be.
So I guess the question is - how would I go about dividing the result set into sections grouped by type?
Currently I'm doing this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Show" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"type" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
I would write numberOfSections method so that it dynamically checks for the number of types. Then, whenever a new type rolls in, just do a [tableView reloadData];

Sort on to-many relationship using a NSFetchedResultsController

I'm trying to use the NSFetchedResultsController in my app, but have a problem to sort my data. I get the following error when trying to sort the result using a relationship that is two levels down from the entity:
* Terminating app due to uncaught exception
'NSInvalidArgumentException', reason:
'to-many key not allowed here'
My data model is set up this way:
Item <<---> Category <--->> SortOrder
<<---> Store
In other words: Each item belongs to one category. Categories can have different sort orders for each store that includes a certain category.
So, I'm creating a fetch request to find all items for a certain store and would like to present the result using category names as sections, and sorted on the sort order.
When I perform the the fetch (last line below), I get the above error.
NSManagedObjectContext* context = [appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(status != %d) AND (ANY category.sort.include == YES) AND (ANY category.sort.store == %#)", ItemStatusDefault, store];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"category.sort.order" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
self.resultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"category.name"
cacheName:nil];
[fetchRequest release];
NSError *error;
BOOL success = [self.resultsController performFetch:&error];
If I change the sorting to, say, category names, it works.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES];
How can I get the NSSortDescriptor to sort on the sort order?
UPDATE:
So it seems this is not possible. I got a suggestion to create a transient property and sort on that, but Apple documentation clearly states
You cannot fetch using a predicate
based on transient properties
My conclusion here is that I cannot use NSFetchedResultsController out of the box. I need to either access the array of objects the NSFetchResultsController gives me and sort in memory, or setup my own fetch requests and skip NSFetchedResultsController.
iOS 5 provide now ordered relationships
https://developer.apple.com/LIBRARY/ios/releasenotes/DataManagement/RN-CoreData/index.html
UPDATE:
Link updated
Reference : "Core Data Release Notes for OS X v10.7 and iOS 5.0"

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'