Core Data Sorted relationship + NSFetchedResultsController - ios

I have a model with Tags and Documents. Each Tag can have multiple Documents. Each document can have multiple Tags. In a Tag all the Documents are ordered. The Tag.Documents relationship is currently an NSSet, so it's unordered.
I am also using a NSFetchedResultsController with a NSSortDescriptor that just returns the Documents in other of their creation-time property. But I need them to be in a different order depending on the Tag I'm currently displaying.
Having looked at multiple other SO questions it seems that using a NSOrderedSet might not offer the solution, so instead I am trying to add a third object TagDocumentPair that points to 1 Tag and 1 Document. Each tag and document will point to multiple TagDocumentPair objects. Pretty straight forward.
The question I have is how should I create the NSSortDescriptor?
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Document" inManagedObjectContext:.managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"tags contains %#",tag];
request.fetchBatchSize = 20;
request.sortDescriptors =
// #[[NSSortDescriptor sortDescriptorWithKey:#"creationTime" ascending:NO]];
This would probably be easy to do with a JOIN...

You are right that a fetch request cannot sort the results according to an ordered relationship, so using an "intermediate" entity might be the only solution.
You would then execute a fetch request on the "TagDocumentPair" entity instead of the "Document" entity:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"TagDocumentPair" inManagedObjectContext:managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"tag == %#", selectedTag];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"orderNumber" ascending:NO]];
where "orderNumber" is an attribute of "TagDocumentPair" that describes the ordering
of the documents of a tag.
Since each "TagDocumentPair" points to exactly one "Document", you can display the
document attributes in cellForRowAtIndexPath etc.
A disadvantage of this solution might be that changes to the attributes of a "Document" will not automatically trigger table view updates, because a fetched results controller does
not track changes to related objects.

Related

Accessing objects using NSPredicate from Core Data

I tried to find answer to my simple question with no luck... I'm working on CoreData and i have two entities lets take an example of "Photo and Photographer" one to many relationship, means one photographer can have multiple photos... Now i did store objects using
[NSEntityDescription insertNewObjectForEntityForName...]
I'm having issues retrieving "all photos" from a specific photographer. I'm using this code
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Photographer"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name = %#",#"photographerName"];
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"timeStamp" ascending:YES]];
this code is returning photographer however i only need ALL photos taken by this photographer.
I'm new to CoreData, i'm not sure if i should be using "Photos" entity?
thanks in advance.
Your fetch request should request Photo entity.
Then, in your predicate, you can specify the name of the photographer like so:
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"photographer.name = %#",#"photographerName"];
Or better yet, it would be much faster to first get the photographer object, and then test the relationship in the predicate:
Photographer* photographer = ... //Get required photographer here
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"photographer = %#", photographer];
Use this to sort the photographer.photos array.
NSSortDescriptor *sortKey = [[NSSortDescriptor alloc] initWithKey:#"timestamp" ascending:YES];
NSArray *sorters = [NSArray arrayWithObject:sortKey];
NSArray *sortedArray = [photographer.photos sortedArrayUsingDescriptors:sorters];

NSFetchedResultsController with data from other model

I'm trying to build app with UITableView with data from NSFetchedResultsController. The problem is - I need to show some additional data from other models in table view.
Assume I have 2 models: Students and Groups (I think the relation is obvious: group consists of many students and student can be only in one group). I'm trying to build UITableView with list of groups. I would also like number of students in each group.
(NSFetchedResultsController *) fetchController {
if (!_fetchController) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"GroupModel" inManagedObjectContext:[NSManagedObjectContext MR_defaultContext]];
[fetchRequest setEntity:entity];
NSPredicate *messageInChatPredicate = [GroupModel getMyChatsPredicate];
[fetchRequest setPredicate:messageInChatPredicate];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
_fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext MR_defaultContext] sectionNameKeyPath:nil cacheName:#"main"];
_fetchController.delegate = self;
}
return _fetchController;
}
I wonder how I can I add additional information (such as number of students in each group)?
Why not just use group.students.count to get the value from the group entity? You only need one fetchedResultsController and I think you will find Core Data performance is fine.
Your terminology is a bit confusing, usually a model is a core data model created in Xcode and has a number of entities e.g. Group, Student. Sometimes you can use two different models and/or different versions of the models.
Your objects are usually know as Entities and both would belong to the same Core Data model.
EDIT:
To get the count of subgroups, such as girls or boys you can do the following
NSMutableArray *boys = [[NSMutableArray alloc] init];
[NSPredicate predicateWithFormat:#"gender = %#",#"MALE"];
[boys addObjectsFromArray:[group.students filteredArrayUsingPredicate:predicate]];
NSLog(#" boys count is %d", boys.count);
Assuming of course that gender is an attribute of Student.
Create another NSFetchedResultsController that is responsible for the other model (i.e student).
You have basically two options to get additional information such as the number of students in each group.
The first option is to count the students in the Group-Student relation. To serve this request the NSManagedObjectContext will load all students in memory to get a suitable result. Personally, I don't really like this option.
The second option is to store the number of students who belong to a group in a property of the group so that it can be directly add. Yes, you have to maintain the correct number manually. Depending on the amount of data that has to be loaded, this approach is more preferable. Crucially since it's way fas

iOS: Sorting and grouping NSFetchedResultsController

I have two tables with the following structure
MainCategory:
name position hasSubCategories
SubCategory:
name position display belongsToMainCategory
Now I want to display all subcategories (where display attribute = YES) grouped by main category. The main category sections should be sorted as defined by position and the subcategories itself (within the section) by their position attribute. (name by the way can be the same for a certain main category...my tables have more attributes but they aren't relevant to understand the problem).
But my order is completely messed up. Why? Here's my code for the FetchedResultsController:
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SubCategory"];
NSSortDescriptor *mainCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"belongsToMainCategory.position" ascending:YES];
NSSortDescriptor *subCatPosition = [[NSSortDescriptor alloc]
initWithKey:#"position" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObjects:mainCatPosition,subCatPosition,nil];
request.predicate = [NSPredicate predicateWithFormat:#"display = %#", [NSNumber numberWithBool:YES]];
[self.db.managedObjectContext executeFetchRequest:request error:&error];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.budgetDatabase.managedObjectContext
sectionNameKeyPath:#"belongsToMainCategory.name"
cacheName:nil];
The key path used as sectionNameKeyPath: argument to the fetched results controller must
be the same key that is used in the first sort descriptor or generate the same relative ordering.
The fetched results controller first orders all fetched objects according to the first
sort descriptor and then groups the objects into sections according to the sectionNameKeyPath. Therefore using different key paths (as in your case "belongsToMainCategory.position" vs. "belongsToMainCategory.name") does not work.
This could even cause a runtime error about "out of order sections".

Core Data - Select distinct

I know there have been several discussions about this but none of them resolved my simple problem.
I have an Entity called Character and inside there are 4 columns:
character_id, episode_id, title, desc
there can be several same character_ids values but with different episode_id.
When I perform fetch\select I do it for whole table and wishes to get it distinctly by character_id. so this is what I do:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
// Add a sort descriptor. Mandatory.
if(sortDescriptors != nil) {
[fetchRequest setSortDescriptors:sortDescriptors];
}
fetchRequest.predicate = predicate;
// Required! Unless you set the resultType to NSDictionaryResultType, distinct can't work.
// All objects in the backing store are implicitly distinct, but two dictionaries can be duplicates.
// Since you only want distinct names, only ask for the 'name' property.
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = [NSArray arrayWithObject:[[entity propertiesByName] objectForKey:#"title"]];
fetchRequest.returnsDistinctResults = YES;
NSArray *fetchResults = [moc executeFetchRequest:fetchRequest error:&error];
The 'fetchResults' array contains 3 out of 10 rows which is the right result!
The problem: None of the object within the array is accessible.
If I try the following:
NSDictionary item1 = [fetchResults objectAtIndex:0];
NSString *title = [item1 objectForKey:#title"];
I get an exception!
What am I doing wrong?? how can I translate back the dictionary into NSManagedObjects??
Thank you!
First, when using Core Data you should not use foreign keys. Rather, it is preferable to use Core Data's relationships and let the framework deal with the ids in an opaque manner. Maybe you are synching with a web service and need to keep track of the ids but your program logic actually should not depend on it.
Second, if you need an object, it is really better to use the NSManagedObjectResultType rather than the NSDictionaryResultType. You can still obtain distinct results. If you are not experiencing performance issues, this is the preferred pattern. The code is also much more readable.
Your data structure would be this, with a many-to-many relationship:
Character <<--->> Episode
All characters of an episode or all episodes with a certain character is simple. These will be "distinct" results dictated by the logic of the data model:
NSArray *allCharactersInEpisode = episode.characters;
NSArray *allEpisodesWithCharacter = character.episodes;
To select all characters of all episodes you just select all characters. Much simpler than a "distinct" query.

Value of sectionNameKeyPath for many-to-many relationship in Core Data

I have a Core Data model with three entities: Notification, Group and Customer. These are the relationships between them:
A customer belongs to many groups and a group can have many customers.
A notification is sent (belongs) to a group and a group can receive (have) many notifications.
I would like to show all notifications in a UITableView grouped by customer. I have created a NSFetchedResultsController like this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.fetchBatchSize = 10;
fetchRequest.predicate = nil;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notification"
inManagedObjectContext:self.managedObjectContext];
fetchRequest.entity = entity;
// Default sort descriptors are built in a separate custom method
NSArray *sortDescriptors = [self getDefaultSortDescriptorsForEntity:entity];
fetchRequest.sortDescriptors = sortDescriptors;
return [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"group.customers.firstName"
cacheName:nil];
Assuming this is a valid way to retrieve all notifications grouped by customer (I'm not sure about that either) iOS is throwing the following exception:
#"Failed to fetch all Notification objects"
#"Reason: Invalid to many relationship in setPropertiesToFetch: (group.customers.firstName) (NSInvalidArgumentException)"
I have reviewed the relationships again and again to see if something is missing and everything seem to be correct. I can create and delete objects for all entities and links between them are correct as well.
My question is: Is it possible to traverse through several relationships in a sectionNameKeyPath value? How should many-to-many relationships be handled in this kind of scenario?
Yes, you can do this. Just fetch the customers with the FRC and set the sectionNameKeyPath to nil.
Sections
The number of returned results is your number of sections. Populate the section headers with the customer data.
Rows
The number of rows in section would be customer.notifications.count. To populate the row, make sure the notifications are sorted in some way (say, by date) and display them accordingly with something like this:
NSArray *orderedNotifications =
[customerForSection.notifications sortedArrayUsingDescriptors:
#[[NSSortDescriptor sortDescriptorWithKey:#"date" ascending:NO]]];
Notification *notificationToBeDisplayed =
[orderedNotifications objectAtIndex:indexPath.row];
An alternative - recommended - solution is to change the data model. You could associate the notification with all customers directly. That would have the added advantage, that notifications remain associated with the correct customers even if group memberships change.

Resources