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];
Related
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]]]
Some background info on my datamodel:
manufacturer <-->> item <<-->> tag
I currently generate a list of items by a fetchrequest:
- (NSFetchRequest*) rankingRequestForItem:(Item*)item {
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:#"Item"];
NSPredicate* p = [NSPredicate predicateWithFormat:#"SELF != %#",item.objectID];
r.resultType = NSDictionaryResultType;
r.resultType = NSDictionaryResultType;
r.propertiesToFetch = #[[self objectIDExpressionDescription],#"itemName",
[self rankingExpressionDescriptionForTags:[item mutableSetValueForKey:#"itemToTag"]]];
r.predicate = p;
r.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"itemName" ascending:YES]];
return r;
}
This generates a list of all items. I want to filter it for items that have a relationship to a specific manufacturer. So I'm adding a predicate after the listing of all items and it sorts by selectedManufacturer.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"itemToMa = %#", selectedManufacturer];
This works, but is grabbing a lot of items that will be filtered out. With large data sets I'm assuming will become slower and slower as it searches all items rather than just the ones associated with one manufacturer. I want to filter for items within the initial 'rankingRequestForItem' method.
Is it possible to move the above predicate with the top predicate and create a compoundpredicate?
I would not worry about performance. Core Data manages that pretty well under the hood. Sometimes the order of the predicates matters, so maybe put the manufacturer filter first.
You can combine the predicates in one as suggested in the comment to your question, or use compound predicates -- the result is pretty much the same.
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"
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.
My app has smart folder like functionality: a predicate is setup with a NSPredicateEditor and used to fill the folder with a fetch request.
The entity used in the search has a to-many relationship. The relationship is ordered, in the sense that an index is stored in the destination entity for sorting purposes.
My problem is that I would like to build in a rule based on the last values in the ordered relationship, but I can't figure out how to build a predicate to do this, because the relationship is not an array. Core data doesn't actually know about the order.
I have a readonly property on the class that returns the ordered items, but this doesn't seem to help with the fetch request because the property is not available in the core data store.
The only option I can think of is to de-normalize and store the last items in the relationship ordered in a separate property. Is that the only solution?
Well, assuming I have understood the problem correctly, I'd do it like this. Lets say you've got two entities, TopEntity has a (NSString *)name property and a to-many relationship to MyEntity which has a (NSString *)data property and (NSInteger)order property.
Lets say you want the TopEntity objects which match a given string, and whose MyEntity orders are satisfy a certain condition, then you can do it with two predicates and an NSFetchRequest like so....
NSManagedObjectContext *context = [self managedObjectContext];
// Create some top level entities
TopEntity *aTop = [TopEntity insertInManagedObjectContext:context];
aTop.name = #"This is Your Name";
TopEntity *bTop = [TopEntity insertInManagedObjectContext:context];
bTop.name = #"This aint a Name";
TopEntity *cTop = [TopEntity insertInManagedObjectContext:context];
cTop.name = #"This is My Name";
// Add some data
NSInteger i, len = 30;
for(i=0; i<len; i++) {
// Create a new object
MyEntity *entity = [MyEntity insertInManagedObjectContext:context];
entity.orderValue = i;
entity.data = [NSString stringWithFormat:#"This is some data: %d", i];
if(i < 10) {
[aTop addObjectsObject:entity];
[entity addTopObject:aTop];
} else if (i < 20) {
[bTop addObjectsObject:entity];
[entity addTopObject:bTop];
} else {
[cTop addObjectsObject:entity];
[entity addTopObject:cTop];
}
}
// Save the context
NSError *error = nil;
[context save:&error];
// A predicate to match against the top objects
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH %#", #"This is"];
// A predicate to match against the to-many objects
NSPredicate *secondPredicate = [NSPredicate predicateWithFormat:#"ANY objects.order < %d", 5];
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
[fetch setEntity:[NSEntityDescription entityForName:#"TopEntity" inManagedObjectContext:context]];
[fetch setPredicate:predicate];
NSArray *result = [[context executeFetchRequest:fetch error:&error] filteredArrayUsingPredicate:secondPredicate];
for(TopEntity *entity in result) {
NSLog(#"entity name: %#", entity.name);
}
So, essentially you can just wrap the results of your fetch request with another predicate and use the ANY keyword.
I've got no idea how efficient that is, but it works for this case. Running the above will output "This is Your Name" i.e. it matches the first TopEntity.
I don't think there's a way to limit to n results in a predicate, only at the fetch request level.
Aside from referencing the last n items in a relationship as you mentioned, you might try a boolean attribute "lastN" and flip them on/off when you curate the order of the list (say, during user-initiated sort or drag-and-drop reordering).
Alternatively, you could create a separate fetch request for each searched thing that sorts by your sort key, ordered descending, and is limited (via -setFetchLimit: ) to n results.
Tracking this as a relationship or an attribute is somewhat "messy" whereas the fetch limit is more expensive (because of multiple round trips). If your reordering is done by one-off user actions, it might be better performance-wise to use the relationship or attribute approach since the work is amortized rather than done all at once in a series of fetches. I haven't found a better way myself and will follow this one closely. :-)