Sorting NSFetchedResultsController using a to-many relationship property - core-data

SCENARIO
I have two entities: Item and ListDetail (which contains prices for different lists for every item). This is absolutely needed and I can't provide a price attribute for the Item entity because every item can have more prices for different dynamic lists (retail, b2b ecc.).
The relationship is:
Item (lists) <------->> (item) ListDetail
The current active list in my app change dinamically, so let's say I have an integer variable with the current active list: _ACTIVE_LIST_CODE_. When I need a price for an item object I use an helper method on the Item class:
-(NSNumber*) getPrice {
NSSet *lists=[self.lists filteredSetUsingPredicate: [NSPredicate predicateWithFormat:#"listId == %d",_ACTIVE_LIST_CODE_]];
ListDetail *activeList=[[lists allObjects] objectAtIndex:0];
return activeList.price;
}
THE PROBLEM
I use a UITableView with NSFetchedResultController in order to select and show some items for different sections. Nothing special. I would like to order the fetchedObjects using the items price for the active list. If price was an attribute of Item I would added simply a sort descriptor to the fetch request like so:
[NSSortDescriptor sortDescriptorWithKey:#"price" ascending:YES];
But as said before this is not possible, price is a dynamic attribute.
If using transient properties was possible for sort descriptors, I would set a price transient properties calculated on fly using my helper method. Nothing to do.
Using a keypath in the descriptor like "lists.price" is not possible (or maybe I don't know how to do that), just because it's a to-many relationship and it's modeled with a NSSet.
I tried some workaround, without success:
1) observing _ACTIVE_LIST_CODE_ changes to set items price in a non-transient attribute.
2) after the fetch request, before presenting the table view, reorder a brand new array with fetched objects using the transient "price" property, iterate the orderdered array following an ascending integer index "i" and assigning this value to a non-transient property "order" for the Item entity. Using "order" for sort descriptor in the fetch request. (This approach is described here: Re-ordering NSFetchedResultsController)
Both of them works, but they slow down performance because I have thousands of items in the fetch results... Any idea?

How about fetching ListDetail instead? You could restrict and sort with the appropriate predicates and sort descriptors, exactly as you propose.
fetchRequest.predicate =
[NSPredicate predicateWithFormat:#"listID = %#", activeListCode];
fetchRequest.sortDescriptors =
#[[NSSortDescriptor sortDescriptorWithKey:#"price" ascending:YES]];
Now, to group by some attribute of item should be simple and efficient because it is a to-one relationship. Your fetched results controller's sectionNameKeyPath can be something like
#"item.category"

Related

Core Data - NSFetchRequestResult: count of items in one-to-many relationship from result?

Consider a Core Data model with two Entities: TermDictionary and Term. The TermDictionary has a "name" property, and a one-to-many relationship called "terms" which points to a set of Term objects, each of which consists of two properties: "name" and "score".
I've got an NSFetchRequest which I'm using as a data source for a UITableView which displays all of the TermDictionaries in the database. The idea is that the table will, for each cell, display the name of the dictionary, along with a count of the number of terms in that dictionary.
In the following code snippet, item contains an NSFetchRequestResult for the "TermDictionary" entity:
let thisDict = item as! TermDictionary
cell.textLabel?.text = thisDict.name
cell.detailTextLabel?.text = "\(thisDict.terms?.count ?? 0) terms"
...The table cells are correctly displaying the names of the Term Dictionaries, however it looks like thisDict.terms is always coming up nil, so the number-of-terms label always shows zero.
Do I need do do something special with item rather than just casting it to my TermDictionary managed object subclass?
You do not need to do anything special. If thisDict.terms prints as nil, it really is nil. Check your data store.

NSFetchRequest to-many key not allowed here

I have a many to many relationship between two entities; A and B.
I want to return an array of B.relationship for every A where B.relationship’s count is greater than 0 and sorted by B’s dateCreated property.
This is the code I currently have which probably makes a little more sense.
let fetchRecentVariationsRequest = NSFetchRequest(entityName: "Variation")
fetchRecentVariationsRequest.predicate = NSPredicate(format: "ANY activities.#count > 0")
fetchRecentVariationsRequest.sortDescriptors = [NSSortDescriptor(key: "activities.dateCreated", ascending: true)]
When I run the request I get the following exception:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'to-many key not allowed here’
I understand why I’m getting the error but how I sort by a to many property for a sort descriptor in Core Data?
Edit
To be more clear, I’d like to fetch the 6 most recent Activity entities sorted by their dateCreated property (newest first).
Then I’d like to fetch all of the Variation entities which are related to these fetched Activity entities via the Activity entity’s variations relationship.
You can't sort by the attribute of a to-many relationship, because it makes no sense. CoreData needs to decide which Variation to put first. Your sort descriptor says "use the value of dateCreated on the related Activities". But there are several Activities and so several different dateCreated values for each Variation. Which Activities' dateCreated should it use? The last? The first? The average?
But over and above that conceptual problem, CoreData will only allow you to use an attribute, or a to-one relationship, to sort by (at least for a fetch from the SQLite store). No transient properties; no computed properties. So if you want to use the dateCreated of the most recent related Activity, you will need to add an attribute to Variation which you update every time an Activity is added to the relationship.
EDIT
Given your update, I would fetch the most recent six Activities first:
fetchRecentActivitiesRequest = NSFetchRequest(entityName: "Activity")
fetchRecentActivitiesRequest.sortDescriptors = [NSSortDescriptor(key: "dateCreated", ascending: false)]
fetchRecentActivitiesRequest.fetchLimit = 6
// I recommend using the next line to fetch the related Variations in one go
fetchRecentActivitiesRequest.relationshipKeyPathsForPrefetching = ["variations"]
let recentActivities = try! context.executeFetchRequest(fetchRecentActivitiesRequest) as! [Activity]
and then use the variations relationship to get the corresponding Variations:
let nameSort = NSSortDescriptor(key: "name", ascending: true)
let recentVariations = recentActivities.flatMap() {
$0.variations!.sortedArrayUsingDescriptors([nameSort])
}

Core Data NSFetchRequest Sort by Category Method Return Value

How do I sort my fetched results by a value that is returned by a method in a category of the entity I'm fetching?
In my category, I sum up several values from the entity's to-many relationship, then divide by the number of objects in the relationship, effectively creating an average that I return in my category method as a float value.
Here is my code:
In the Category.h
- (float)smallPenaltyAvg;
In the Category.m
- (float)smallPenaltyAvg{
float smallPenaltyAvg = 0;
for (Match *mtch in self.matches) {
smallPenaltyAvg += [mtch.penaltySmall floatValue];
}
if ([self.matches count] > 0) {
smallPenaltyAvg = (float)smallPenaltyAvg/(float)[self.matches count];
}
return smallPenaltyAvg;
}
And when I call it in the Core Data Table View Controller class that I created...
NSFetchRequest *poolRequest = [[NSFetchRequest alloc] initWithEntityName:#"Team"];
poolRequest.predicate = [NSPredicate predicateWithFormat:#"regionalIn.name = %#", _regionalToDisplay];
poolRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"smallPenaltyAvg" ascending:YES]];
And I have the Category.h file imported on every file previously mentioned outside of the Category.h file itself.
It gives me the error of:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'keypath smallPenaltyAvg not found in entity <NSSQLEntity Team id=5>
Am I not allowed to do this?
If I am, what am I doing wrong?
I do not think this has anything to do with the kind of persistent store.
The trick is to create an appropriate attribute in the managed object model, and mark it as Transient. Then override the getter of this attribute to do your calculations.
Now your fetch request should work as expected (although there are some caveats with fetched results controllers).
As for the SQLite problem, when you add the SQLite store with
- (NSPersistentStore *)addPersistentStoreWithType:(NSString *)storeType
configuration:(NSString *)configuration
URL:(NSURL *)storeURL
options:(NSDictionary *)options
error:(NSError **)error
just pass NSSQLiteStoreType as the storeType. The other options are binary and in-memory, so in this sense this is indeed the "default".
This is not possible when using a backing SQLite store.
My suggestion is you persist the average property, and maintain it yourself by overriding the Match setCategory: property and making the calculation there for every match added.
What I did to solve my problem was create a new attribute for every average or sum that I needed in the Team object from all of its Match objects' attributes and then created a method in the TeamCategory file that populated those averages and that method was called every time a Match object was inserted into the Team object. It took a while to do, but it works now. If there is a better solution, I'm still open for suggestions.

Filtering a relationship of an NSManagedObject

Suppose a Manager has a to-many relationship with Employee objects. Given a reference to a Manager object (i.e. NSManagedObject *manager), how do I get a reference to "the Employee with the lowest salary among all of those whose salaries exceed 10000"?
I can think of two possible approaches:
Approach 1: constructing an NSFetchRequest and specifying the Manager wanted with the object ID of the Manager in question.
Approach 2: some kind of key-value coding expression on Manager, e.g. [manager valueForKeyPath:#"..."] (with some use of NSPredicate?)
I'm inclined towards Approach 2 if there's a plausible solution. Please enlighten me.
Of course, you can just apply a predicate and sort descriptor to the set returned by the relationship. Easy, and pretty quick if the set is relatively small (because it is done in memory, all the objects will have to be fetched). You may want to do that batch fetch up front to limit the number of times you do I/O.
Depending on the size of the database and the number of employees under the manager (and the indexing), you may want to do it all at the database level...
// We want "Employee" objects
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Employee"];
// Assuming the "inverse" relationship from employee-to-manager is "manager"...
// We want all employees that have "our" manager, and a salary > 10000
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"(manager == %#) AND (salary > 10000", manager];
// Sort all the matches by salary, little-to-big
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"salary" ascending:YES]];
// Limit the returned set to 1 object, which will be the smallest
fetchRequest.fetchLimit = 1;
This will perform the entire query at the database level, and only return 1 object.
As always, performance issues are usually highly dependent on your model layout, and the options used for specifying the model and its relationships.
You can filter your array of Employee relationship to get the one you want.
1) First, get all the Employee with salaries over 10000:
NSArray *filtered = [manager.employees filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"(salary > 10000)"]];
2)Then sort it in descending order
NSSortDescriptor* sortOrder = [NSSortDescriptor sortDescriptorWithKey: #"salary" ascending: NO];
NSArray *sortedAndFiltered = [filtered sortedArrayUsingDescriptors: [NSArray arrayWithObject: sortOrder]];
3)Then just get your employee
[sortedAndFiltered lastObject];

What is the correct way for quering a NSNumber in a NSArray within CoreData?

I have a Entity with a column of type ID named "responsibleUsers". In this column I store an Array containing NSNumbers.
I want to fetch all objects of this entity, that match my current User. Therefore i create following predicate:
[NSPredicate predicateWithFormat: #"%# IN responsibleUsers",[NSNumber numberWithInteger: curUser.oID.integerValue] ]
whatever I try, my App crashes. Once with a EXC_BAD_ACESS, once with "unimplemented SQL generation for predicate nsnumber"
What is the correct way to query my entity?
The query you are trying assumes that you have two entities: the entity you querying (let's call it Group) and another one, perhaps called User, which is set up as a to-many relationship from Group called responsibleUsers. You would then be able to use the predicate you suggest:
[NSPredicate predicateWithFormat:#"%# IN responsibleUsers, aUser];
This would be the recommended use of Core Data object graphs.
In your setup, it seems you have an array of NSNumber set as a property rather than a relationship. Therefore you cannot use such a query. You simply have to retrieve the array and query the array.
BOOL containsResponsibleUser = NO;
for (NSNumber *n in aGroup.responsibleUsers) {
if ([n isEqualTo:[NSNumber numberWithInteger: curUser.oID.integerValue]])
containsResponsibleUser = YES;
}
If you are indeed querying something like a group of users, I would recommend the first approach. If you are querying some kind of user, I would suggest a BOOL property responsible as the most efficient solution.

Resources