I have the following data model in my app:
Basically I want to store country names aswell as city names. Each city belongs to one country and one country contains 0 - n cities.
Before I now add new cities to a certain country I need to know if the country already contains a city with this name.
Until now I do this:
- (BOOL)countryForName:(NSString *)countryName containsCity:(NSString *)cityName {
Countries *country = [self countryForName:countryName];
NSSet *cityNames = [country valueForKey:#"cities"];
for (Cities *city in cityNames) {
if ([city.cityName isEqualToString:cityName]) {
return YES;
}
}
return NO;
}
This obviously is very slow, what I need is an equivalent fetch with a correct predicate. I perform a search for one entity like this:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Countries" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"countryName" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
fetchRequest.sortDescriptors = 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:#"GetCountryList"];
aFetchedResultsController.delegate = self;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"countryName == %#", countryName];
[aFetchedResultsController.fetchRequest setPredicate:predicate];
But how do I do this if a search involves multiple entities? Or in other words: How do I add unique city names for one country only?
If you want to check that a given country object already contains a city with a name,
you can do
Countries *country = ...
NSString *cityName = ...
if ([[country valueForKeyPath:#"cities.name"] containsObject:cityName]) {
// ...
}
Or, using a fetch request:
NSString *countryName = ...
NSString *cityName = ...
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Countries"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"countryName == %# AND ANY cities.cityName == %#", countryName, cityName];
[fetchRequest setPredicate:predicate];
NSError *error;
NSUInteger count = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error];
if (count == NSNotFound) {
// error
} else if (count == 0) {
// no matching country
} else {
// at least one matching country
}
Note that a NSFetchedResultsController is normally used to display the contents of a
fetch request in a table view, so it is not needed here.
Related
I am looking for a predicate to fetch all managed objects of type Entity whose values are duplicated in a property sessionId, where all groups' ("groups", meaning managed objects whose sessionId's are equal) contents' flags in a property processed is set to YES. This can be done (slowly), but I am looking for an efficient one liner for this. Thanks
This is the slow way:
NSFetchRequest *request = [NSEntityDescription entityForName:#"Entity"
inManagedObjectContext:context];
NSArray *all = [context executeFetchRequest:request error:nil];
NSArray *sessionIds = [all valueForKeyPath:#"#distinctUnionOfObjects.sessionId"];
NSMutableArray *objects = [NSMutableArray array];
for (NSString *sessionId in sessionIds) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"sessionId == %#", sessionId];
NSArray *inSession = [all filteredArrayUsingPredicate:predicate];
for(id obj in inSession) {
if(![obj valueForKey:#"processed"]) continue;
}
[objects arrayByAddingObjectsFromArray:processed];
}
NSLog(#"%#", objects);
Since booleans are stored as 0s and 1s, a group where all rows have processed = YES will have average(processed) = 1. Hence you can use NSFetchRequest's propertiesToGroupBy and havingPredicate to get the sessionIds that meet your criteria. A second fetch is then required to get the Entity objects with any of those sessionIds:
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = #[#"sessionId"];
fetch.propertiesToGroupBy = #[#"sessionId"];
fetch.havingPredicate = [NSPredicate predicateWithFormat: #"average:(processed) == 1"];
NSArray *resultsArray = [context executeFetchRequest:fetch error:nil];
NSArray *sessionIdArray = [resultsArray valueForKeyPath:#"sessionId"];
NSFetchRequest *newFetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
newFetch.predicate = [NSPredicate predicateWithFormat:#"name IN %#",sessionIdArray];
NSArray *finalResults = [context executeFetchRequest:newFetch error:nil];
NSLog(#"Final results, %#", finalResults);
Sorry it's not a one-liner. And I leave it to you to determine whether it's any quicker than your own code.
EDIT
To do it all in one fetch, use NSFetchRequestExpression in place of the intermediate arrays:
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = #[#"sessionId"];
fetch.propertiesToGroupBy = #[#"sessionId"];
fetch.havingPredicate = [NSPredicate predicateWithFormat: #"average:(processed) == 1"];
NSExpression *fetchExpression = [NSFetchRequestExpression expressionForFetch:[NSExpression expressionForConstantValue:fetch] context:[NSExpression expressionForConstantValue:context] countOnly:false];
NSFetchRequest *newFetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
newFetch.predicate = [NSPredicate predicateWithFormat:#"sessionId IN %#",fetchExpression];
NSArray *finalResults = [context executeFetchRequest:newFetch error:nil];
NSLog(#"Final results, %#", finalResults);
Note that on my (admittedly trivial) test setup this actually ran more slowly than the two-fetch solution.
FYI, if you use the SQLDebug build setting to examine the SQL that is generated, it looks something like this:
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZSESSIONID, t0.ZPROCESSED FROM ZENTITY t0 WHERE t0.ZSESSIONID IN (SELECT n1_t0.ZSESSIONID FROM ZENTITY n1_t0 GROUP BY n1_t0.ZSESSIONID HAVING avg( n1_t0.ZPROCESSED) = ? )
This code currently lists all my categories. However I will like to filter this result to display specific results. For example... The array contains the items Jamaica, Japan, Germany and Asia. I will like to filter the displayed results to only show Japan and Germany. I've read NSPredicate can assist me but i'm not too sure on how to implement it here.
Datacontroller.M
+(NSArray*) getCategories {
AppDelegate* delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
NSManagedObjectContext* context = delegate.managedObjectContext;
NSError *error;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setReturnsObjectsAsFaults:NO];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"StoreCategory" inManagedObjectContext:context];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"created_at" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
return fetchedObjects;
}
Categories.m
-(UITableViewCell*)MGListView:(MGListView *)listView1 didCreateCell:(MGListCell *)cell indexPath:(NSIndexPath *)indexPath {
if(cell != nil) {
StoreCategory* cat = [listViewMain.arrayData objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.backgroundColor = [UIColor clearColor];
[cell.labelTitle setText:cat.category];
[self setImage:cat.category_icon imageView:cell.imgViewThumb];
}
return cell;
}
You cannot filter the unwanted category in this method because indexPath will be a sequence of indices.
You will have to build another array that includes the category you want before UITableView delegate or DataSource functions are called, for example, in ViewDidLoad.
This method seemed to do the trick.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"category CONTAINS[cd] %# OR category CONTAINS[cd] %#", #"Japan", #"Germany", context];
[fetchRequest setPredicate:predicate];
Let's say my data model has two entities: Department and Person. Departments has a to-many relationship called 'departmentToPerson' with Person. Person has a to-one relationship called 'personToDepartment' with Department.
I want to populate an array of people belonging to a selected department. To selected the department I've created a UILabel that displays a departmentName that is selected from a popup tableview. When I run the app the log shows:
personToDepartment.departmentName == (entity:
Department; id: 0x8cc1920
;
data: {
departmentName = Marketing;
departmentToPerson = "";
I've read the purpose of faults and implemented setRelationshipKeyPathsForPrefetching, but still I get the fault. I'll put in the disclaimer that I'm a newbie and may be missing something obvious. When I delete the predicate my table view populates with all personName. Here's the code:
- (NSFetchedResultsController *)fetchedResultsController {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Person" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment.departmentName = %#", selectedDepartment];
NSLog(#"predicate is: %#",predicate);
[fetchRequest setPredicate:predicate];
[fetchRequest setRelationshipKeyPathsForPrefetching:[NSArray arrayWithObjects: #"departmentToPerson", nil]];
fetchRequest.returnsObjectsAsFaults = NO;
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"personName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Again, I select a new department from the UILabel, the log displays selectedDepartment, but then states relationship fault and does not populate the table.
Thanks in advance for the help!
Updated 9JUNE
I've also found this works:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment = %#", selectedDepartment];
I'm also kind of newbie to CoreData, but it seems to me that predicate is wrong. According to Log record you're about to comparing personToDepartment.departmentName to a department Object. Predicate should look like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"personToDepartment.departmentName = %#", selectedDepartment.departmentName];
But there is a better way. selectedDepartment.departmentToPerson will return an NSSet with all persons belonging to this department (if relationship was set previously). But warning, if you'll try to NSLog(#"%#", selectedDepartment.departmentToPerson) probably you'll get "relationship fault", because CoreData will not do fetch until you address specific object in NSSet or enumerate through it. But for example NSLog(#"%d", selectedDepartment.departmentToPerson.count) will force CoreData to make fetch and you'll see number of persons in department.
UPDATE:
NSSet is empty probably because you are not setting relationship, when creating person's object. Inverse To-Many relationship from department to persons will be set automatically.
- (id)insertNewPerson:(NSDictionary *)personInfo inDepartment:(Department*)departmentObject
{
Person *personObject = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
personObject.name = personInfo.name;
personObject.departmentName = departmentObject.name;
...
personObject.personToDepartment = departmentObject; //this is RELATIONSHIP, if set, then you can access all persons from specific department by doing: departmentObject.departmentToPerson
}
I am actually new to iPhone programming and have successfully create an app but I can't figure out how to retrieve the values stored in NSSet. I have 2 entities in core data related to each other. Users one-to-many to Scores. Users entity has firsNname and lastName attributes and Scores entity has onTime, wasAbsent, and dateIn attributes. I fetch using predicate based on firstName and lastName and then execute the fetch. The fetch is successful and I am able to get both entities in one fetch call. However I cannot get values for the values from Scores entity. Whatever I do, it always returns NSSet object. What I want to do is to retrieve the boolean value which was stored in onTime and wasAbsent attributes and feed them to UISwitch.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
self.managedObject = delegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Student"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"firstname == %# AND lastname == %#", #"James", #"Smith"];
NSError *error = nil;
self.scoresArray = [self.managedObject executeFetchRequest:fetchRequest error:&error];
if ([self.scoresArray count]) {
Student *student = [self.scoresArray objectAtIndex:0];
NSSet *score = student.scores;
NSArray *arr = [score allObjects];
NSLog(#"%#", [arr objectAtIndex:0]);
}
if I can directly access the Score entity instead of using NSSet, that would be ideal that way I can reference it using a dot notation.
Any help would be appreciated. Thanks
Use something like the following:
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
self.managedObjectContext = delegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Student"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"firstname == %# AND lastname == %#", #"James", #"Smith"];
NSError *error = nil;
NSArray *studentArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (Student *student in studentArray) {
Student *student = [self.studentArray objectAtIndex:0];
NSLog(#" student is %# %#", student.firstname, student.lastname);
for (Score *score in student.scores) {
// Do something with score
NSLog(#" score is %#", score);
}
}
Because Student has to-many relationship to Scores (and the property for it is scores), when you get the value of student.scores, it returns an unordered collection, which is NSSet in Foundation. So you just work with it like you work with NSSet and with collections in general.
I have look around for something similar with no luck so I am going to try and explain my trouble and paste some code. I have an application that uses Core Data and I can save and retrieve data from their respective textFields with the exception of my (to many relationships). I believe these are saved and returned as sets when fetched. I have read up on NSSet and looked at some code but with still do not understand how to code it.
Thanks for any help.
Hudson
- (IBAction) findContact
{
AppDelegate *appDelegate = [[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Place"
inManagedObjectContext:context];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
[fetchRequest setEntity:entity];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"placeName = %#", placeName.text];
[fetchRequest setPredicate:pred];
Place *matches = nil;
NSError *error;
NSArray *objects = [context executeFetchRequest:fetchRequest error:&error];
if ([objects count] == 0) {
status.text = #"No matches";
}else{
matches = [objects objectAtIndex:0];
address.text = [matches valueForKey:#"address"];
city.text = [matches valueForKey:#"city"];
state.text = [matches valueForKey:#"state"];
zip.text = [matches valueForKey:#"zip"];
phone1.text = [matches valueForKey:#"phone1"];
email.text = [matches valueForKey:#"email"];
website.text = [matches valueForKey:#"website"];
phone2.text = [matches valueForKey:#"phone2"];
about.text = [matches valueForKey:#"about"];
photoName.text = [[matches photo]valueForKey:#"photoName"];
status.text = [NSString stringWithFormat:#"%d matches found", [objects count]];
NSSet *groupForSections = [groupForSections valueForKey:#"sections"];
for (Group *group in groupForSections) {
NSLog(#"group name = %#", [groupForSections valueForKey:#"groupName"]);
groupName.text = [group valueForKey:#"groupName"];
NSSet *sectionForPlaces = [sectionForPlaces valueForKey:#"places"];
for (Section *section in sectionForPlaces) {
sectionName.text = [section valueForKey:#"sectionName"];
NSLog(#"section name = %#", [section valueForKey:#"sectionName"]);
}
}
}
}
![enter image description here][1]
According to your follow-up comments, Place does not actually have a to-many relationship to Section, and Section does not actually have a to-many relationship to Group. Instead, Group has a to-many relationship (sections) with Section and section's inverse to-1 relationship is called group. Also, Section has a to-many relationship (places) with Place and Place's inverse to-1 relationship is called section.
If all those assumptions are correct (and without seeing your model it's hard to know if that is correct), your code should now be something like:
NSManagedObject *sectionForPlace = [matches valueForKey:#"section"];
sectionName.text = [sectionForPlace valueForKey:#"sectionName"];
NSManagedObject *groupForSection = [sectionForPlace valueForKey:#"group"];
gromName.text = [groupForSection valueForKey:#"groupName"];
This code doesn't directly access the fields through the NSManagedObject classes you've created, which should also work if you prefer that way.