NSFetchRequest to-many key not allowed here - core-data

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])
}

Related

In Core Data, how sort an NSFetchRequest depending on the sum of an attribute of a child entity? (SwiftUI)

I am building an iOS app in SwiftUI for which I have a Core Data model with two entities:
CategoryEntity with attribute: name (String)
ExpenseEntity with attributes: name (String) and amount (Double)
There is a To-many relationship between CategoryEntity and ExpenseEntity (A category can have many expenses).
I’m fetching the categories and showing them in a list together with the sum of the expenses for each category as follows: Link to app screenshot
I would like to add a sort to the fetch request so the categories appear in order depending on the total amount of their expenses. In the example of the previous picture, the order of appearance that I would like to get would be: Tech, Clothes, Food and Transport. I don’t know how to approach this problem. Any suggestions?
In my current implementation of the request, the sorted is done alphabetically:
// My current implementation for fetching the categories
func fetchCategories() {
let request = NSFetchRequest<CategoryEntity>(entityName: "CategoryEntity")
let sort = NSSortDescriptor(keyPath: \CategoryEntity.name, ascending: true)
request.sortDescriptors = [sort]
do {
fetchedCategories = try manager.context.fetch(request)
} catch let error {
print("Error fetching. \(error.localizedDescription)")
}
}
You don't have to make another FetchRequest, you can just sort in a computed property like this:
(I assume your fetched results come into a var called fetchedCategories.)
var sortedCategories: [CategoryEntity] {
return fetchedCategories.sorted(by: { cat1, cat2 in
cat1.expensesArray.reduce(0, { $0 + $1.amount }) >
cat2.expensesArray.reduce(0, { $0 + $1.amount })
})
}
So this sorts the fetchedCategories array by a comparing rule, that looks at the sum of all cat1.expenses and compares it with the sum of cat2.expenses. The >says we want the large sums first.
You put the computed var directly in the View where you use it!
And where you used fetchedCategories before in your view (e.g. a ForEach), you now use sortedCategories.
This will update in the same way as the fetched results do.
One approach would be to include a derived attribute in your CategoryEntity model description which keeps the totals for you. For example, to sum the relevant values from the amount column within an expenses relation:
That attribute should be updated whenever you save your managed object context. You'll then be able to sort it just as you would any other attribute, without the performance cost of calculating the expense sum for each category whenever you sort.
Note that this option only really works if you don't have to do any filtering on expenses; for example, if you're looking at sorting based on expenses just in 2022, but your core data store also has seconds in 2021, the derived attribute might not give you the sort order you want.

How to configure an NSFetchRequest to retrieve a to-many relationship

I've got a Core Data relationship set up like this:
Place PlaceList
===== =========
lists <<--------->> places
I'd like to populate an NSFetchResultsController with a given PlaceList instance's list of places. places is an ordered relationship, and lists is unordered. I'd like the fetch request to return the list of places in the order specified by the relationship.
I started out with this:
let request = NSFetchRequest<Place>(entityName: "Place")
request.predicate = NSPredicate(format: "lists == %#", placeList)
request.sortDescriptors = [NSSortDescriptor(key: "lists", ascending: true)]
When I fetch, though, no places come back. I've tried a bunch of ideas from other answers, trying all different predicates, and most result in exceptions thrown when the fetch request is executed.
I've tried, for example:
"ANY lists == %#"
"lists CONTAINS %#"
"%# IN lists"

Custom ordering Core Data to-many relationship?

How can I sorting by a 'custom' order managed object in a to-many relationship? It seems managed objects are in an arbitrary order, and I need them ordered alphabetically by name property. Shall I extend managed object / entity?
Yes, managed objects in Core Data are in no particular order. You can order them when you fetch by setting the sortDescriptors property on the fetch request. If you are already doing a fetch, that is faster and more efficient than fetching first and then sorting in memory, for example, by calling sortedArrayUsingDescriptors.
So, if you were to fetch all Events, you would do as follows:
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Event.name),
ascending: true)]
Or, if you have a many-to-many relationship from Events to Users that is called usersWhoLiked, you could fetch all events that were liked by at least one user over age 50 as follows:
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "#"SUBQUERY(usersWhoLiked, $user, $user.age > %d).#count != 0",
50))
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Event.name),
ascending: true)]
But, note that the sortDescriptors apply ONLY to the fetch request's entity. And, if you are not doing a fetch at all, for example just ordering all the events of a particular user, then Jon Rose's solution may be just as efficient or more.
It's unclear what exactly you want to order. I hope this answers your question.
The relationship is defined as a set, which is unordered. You can turn the set into an array and then sort the array:
NSArray* sortedEvents = [user.events.allObjects sortedArrayUsingDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]]]

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.

Sorting NSFetchedResultsController using a to-many relationship property

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"

Resources