Core Data One-to-Many Relationship - iphone

I have a one-to-many relationship in my Core Data model. One meal can have multiple foods. I want to populate a table with the the foods from one meal. So if Meal #1 has Apples, Oranges, and Lemons, then the table would consist of those three foods.
Any help is very appreciated because I am completely stuck.
Here is my model:
And the NSManagedObject Classes:
Meal.h
#class Food;
#interface Meal : NSManagedObject
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSSet *foods;
#end
#interface Meal (CoreDataGeneratedAccessors)
- (void)addFoodsObject:(Food *)value;
- (void)removeFoodsObject:(Food *)value;
- (void)addFoods:(NSSet *)values;
- (void)removeFoods:(NSSet *)values;
#end
Food.h
#class Meal;
#interface Food : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) Meal *meals;
#end
Here is the code where I cam trying to attempt this, but I am having some issues.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return _fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Food" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"Meal.foods == %#", entity]];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_context sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}

You mention that you are having issues, but not what the issues are; but your NSFetchRequest predicate doesn't look right to me. Also meals defined on Food is plural but the relationship is To-one -- a little confusing.
Your UITableView should be populated with the Foods from a single Meal, right? Let's call that Meal _thisMeal, then the fetch predicate should be:
NSPredicate *foodPredicate = [NSPredicate predicateWithFormat:#"meals == %#",_thisMeal];
[fetchRequest setPredicate:foodPredicate];
So, the entity you're fetching is Food; and this predicate ensures that their meals property is the Meal your UITableViewController is looking for.
Updated to show how to fetch a named Meal
To fetch a Meal named "Custom Meal":
Meal *_thisMeal = nil;
// this assumes that there should be only one Meal named "Custom Meal"
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Meal"];
NSPredicate *fetchPredicate = [NSPredicate predicateWithFormat:#"name = %#",#"Custom Meal"];
NSError *fetchError = nil;
NSArray *meals = [_context executeFetchRequest:request error:&fetchError];
if( fetchError )
NSLog(#"%s - ERROR while fetching Meal: %#,%#",__FUNCTION__,fetchError,[fetchError userInfo]);
else if( [meals count] != 0 )
_thisMeal = [meals objectAtIndex:0];

Your Meal and Food classes are wrong. It's best to kill those and let XCode auto re-generate
those classes via (make sure your entity is selected):
In your fetchedResultsController getter method, kill the line:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"Meal.foods == %#", entity]];
You would normally use a predicate to filter your data, such as foods that contain a banana. Your current predicate doesn't really make sense at the moment. It's saying "give me meals that contain a food called 'entity',...doesn't make sense.
Since you want to get meals...
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Food" inManagedObjectContext:_context];
should be:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Meal" inManagedObjectContext:_context];
(note the #"Meal").
You would then access the foods from each meal via meal.foods.
This line:
self.fetchedResultsController = theFetchedResultsController;
should be:
_fetchedResultsController = theFetchedResultsController;
What you did is basically call the method you are currently in.

Related

Core Data NSSet Issue

I auto-generated the cored data NSManagedObject class, and what I am trying to do is to display all phone numbers for a certain person. I am trying to display this in my table view class to populate the table.
Person to Numbers is a one-to-many relationship, and the numbers are in the set which I did by the addNumbersObject method. I just do not understand how to fetch this in the fetchresultscontroller and display them in the table view.
Currently I am just fetching all people.
Any ideas or suggestions?
Core Data Class:
#class Number
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSSet *numbers;
#end
#interface Person (CoreDataGeneratedAccessors)
- (void)addNumbersObject:(Number *)value;
- (void)removeNumbersObject:(Number *)value;
- (void)addNumbers:(NSSet *)values;
- (void)removeNumbers:(NSSet *)values;
#end
Table View Class:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return _fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_context sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
Try using a predicate on your fetchRequest.
I am not sure from your question if you want to display all the phone numbers for all the people or how you want that formatted, but this is how to get all the numbers for one person.
To get all the phone numbers for one specific person in one table view:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Number" inManagedObjectContext:_context];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"person.name == %#", personName]]; //Assuming person.name is unique
Then sort that person's phone number like you want it.
There are a lot of ways you might "display all phone numbers for a person" -- think specifically about what you want your table to contain and how it should be organized, and you might have an easier time finding the answer you're looking for.
In the meantime, here's one approach: Use table sections to represent Persons, and rows within each for their associated Numbers. You can do this like so:
Set Number as the entity for your fetch request.
Set "person.name" as the sort key. (You might want to add a second sort descriptor so that Numbers for each person are in a sensible order.)
Also set "person.name" as the section name key path.
Implement your table view data source's -tableView:titleForHeaderInSection: thusly:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}

NSFetchRequest for all children of a parent

How do I fetch all child entities of a parent?
I have a table populated by a parent entity in Core Data. When the user touches a cell I intend to show another table with all children of that parent.
How does the NSFetchRequest look like for this please?
Edit:
model is like this:
student>>dates [one to many, one student have many days]
So I want all dates for any given student (selected by touching in student table cell for that student), then populate dates table with dates for that student.
Thanks!
Assuming that the entity and the class names are Student and Date, and the reverse relationship for Date->Student is called student,
Student *aStudent = ...;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity: [NSEntityDescription entityForName: #"Date" inManagedObjectContext: [aStudent managedObjectContext]]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: #"student == %#", aStudent]];
You don't need a separate fetch request for this. All of the objects from the to-many relationship (don't call them child entities, that is misleading and incorrect) are available by accessing the relationship from the student object - something like student.dates. This gives you an NSSet, you can sort it and turn it to an array if you need to.
Within your first table delegate, when you touch a specific cell, I'll inject the specific parent property to the second table controller. For example:
SecondController secondController = ... // alloc-init
secondController.studentToGrab = ...
where SecondController declaration has a studentToGrab property like the following:
#property (nonatomic, retain) Student* studentToGrab; // use strong with ARC, if non-ARC remember to release it
and in definition synthesize it.
Then in your second controller, within viewDidLoad method you could do:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"YourNameEntityForDate" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"student == %#", studentToGrab];
[fetchRequest setPredicate:predicate];
// you can also use a sortdescriptors to order dates...
NSError *error = nil;
NSArray *resultArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error != nil) {
NSLog(#"Error: %#", [error localizedDescription]);
abort();
}
// use resultArray to populate something...
A remark when you deal with table you could also use NSFetchedResultController class. It has advantages when used for displaying data in tables.
If you have custom classes, you could traverse the generated relationship (return [student dates]). That will get you an unordered NSSet on iOS4, or, you can do it with a fetch request (note I use ARC so no releases/autoreleases here):
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Date"
inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
NSMutableArray *predicates = [NSMutableArray arrayWithCapacity:3];
[predicates addObject:[NSPredicate predicateWithFormat:#"student == %#", aStudent]];
// You might add other predicates
[fetchRequest setPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:predicates]];
// and if you want sorted results (why not, get the database to do it for you)
// sort by date to the top
NSArray *sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"dateAdded" ascending:NO]];
}
[fetchRequest setSortDescriptors:sortDescriptors];
NSError *error = nil;
NSArray *sorted = [moc executeFetchRequest:fetchRequest error:&error];
if (error) {
// Handle the error, do something useful
}
return sorted;

Count Distinct items in Core Data

I have a Core Data entity with a property named 'value' which is often repeated. I wish to retrieve only unique values (done) and also how often each one appears, so that I can sort by that property (I'm building an autocomplete function based on existing user input, so knowing how frequently a certain input has appeared is essential).
My fetch request currently looks like this:
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.managedObjectContext];
NSDictionary *entityProperties = [entity propertiesByName];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:10];
[fetchRequest setFetchLimit:20];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:[entityProperties objectForKey:#"value"]]];
NSSortDescriptor *sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"value" ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"value BEGINSWITH[c] %#",predicateString];
[fetchRequest setPredicate:predicate];
return fetchRequest;
I'm a bit stuck with this one. Any ideas?
Thanks in advance!
I don't like this one, but...
You could do an other fetchRequest with a predicate that exactly match the value you are seeking. Allowing duplicate, then have the count of the array.
This one is better, but more work up front.
An other way could be to have a derived property in your data model that keep track of your duplicated count as you create them.
(with that option you could easily sort by duplicated count)
Ok for a Derived property.
First you will need to subclass NSManagedObject and use that subclass in your data model. (in Xcode 3 there was a way to create that quickly, but I don't know that in Xcode 4) But if you name it the same as your entity I think core data will pick it up.
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSString * firstLetterOfName;
#property (nonatomic, retain) NSString * phoneNumber;
#end
And in your implementation you will need to do something like this (there is code snippet in Xcode 4 to get you started, but there is a typo in one of them, unless it have been corrected)
#import "Person.h"
#implementation Person
#dynamic name, phoneNumber, firstLetterOfName;
- (void)setName:(NSString *)value {
[self willChangeValueForKey:#"name"];
[self setPrimitiveValue:value forKey:#"name"];
self.firstLetterOfName = [value substringToIndex:1];
[self didChangeValueForKey:#"name"];
}
#end
You can see that the firstLetterOfName is set each time the Name is set.
You can do the same kind of thing with relationship.
So when you add an item to a relationship you should be able to look up your relationship.
Here is an exemple of something similar, where I need to find if the object I'm adding have the lowest price of it's group because of a derived property call isMeilleurPrixAvecPrixElment. (this is old code, so I don't recall every detail of it, it have been done in OSX.4)
- (void)addPrixHistoriqueObject:(PrixElement_MO *)value
{
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
[self willChangeValueForKey:#"prixHistorique" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
if ([self isPrixRegulierAvecPrixElement:value])
[self enleveL_AutrePrixRegulierPourCommerceDeCePrixElement:value];
if ([self isMeilleurPrixAvecPrixElment:value])
[self echangeMeilleurPrixAvecCePrixElement:value];
[[self primitivePrixHistorique] addObject:value];
[self didChangeValueForKey:#"prixHistorique" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[changedObjects release];
}
In respond to a comment
depending on your data model and the importance of that aspect in your application, I can think of 3 solutions.
1- Redesing your data model around that aspect.
2- When setting the values query the rest of your entity with a predicate and update a property that have the count.
3- (I'm not sure of that one, but is worth trying) NSManagedObect is an object, so maybe you could have a static dictionary that have the value as a key and the count as value.
I would maybe try number 3 first (it look like the easy one), but I've never done something like that. So I'm not sure for the presitance of a class variable in core data.

Core Data data access pattern?

It seems crazy to me that I have all of these NSFetchRequests for the same NSManagedObjects spread out throughout different view controllers in my app, is there a good pattern for data access that puts what I need in a single place?
I agree it is a bit much, fortunately there is Active Record for Core Data. This makes fetching less tedious, for example, fetching all Person objects from core data would be as simple as
NSArray *people = [Person findAll];
Yes there is, it is called a facade pattern. Simply define a public method on your NSManagedObject subclass like so:
#interface Group : NSManagedObject { }
// … cruft here …
-(NSArray*)peopleSortedByName;
#end
And hide the nasty implementation like so:
-(NSArray*)peopleSortedByName;
{
NSFetchRequest* request = // … bla bla, lots of code here
return [[self managedObjectContext] executeFetchRequest:request
error:NULL];
}
Then use the method just as if the it was any other class in your code. Write once, relief everywhere.
Define a category method for NSManagedObject context which wrappers up a general query into a one-liner.
#interface NSManagedObjectContext(MyQueryAdditions)
-(NSArray *)queryEntityForName:(NSString *)name predicateFormat:(NSString *)pstring argumentArray:(NSArray *)arr;
#end
#implementation NSManagedObjectContext(MyQueryAdditions)
-(NSArray *)queryEntityForName:(NSString *)name predicateFormat:(NSString *)pstring argumentArray:(NSArray *)arr
{
NSEntityDescription *entity = [NSEntityDescription entityForName:name inManagedObjectContext:self];
NSFetchRequest *fetch = [[[NSFetchRequest alloc] init] autorelease];
[fetch setEntity:entity];
NSPredicate *pred;
if(pstring)
{
if(arr) pred = [NSPredicate predicateWithFormat:pstring argumentArray:arr];
else pred = [NSPredicate predicateWithFormat:pstring];
[fetch setPredicate:pred];
}
NSError *error = nil;
[self retain];
[self lock];
NSArray *results = [self executeFetchRequest:fetch error:&error];
if (error) {
NSLog(#"MOC Fetch - Unresolved error %#, %#", error, [error userInfo]);
results = [NSArray array];
}
[self unlock];
[self release];
return results;
}
#end
Means a basic all items query can be as simple as
NSArray *cres = [managedObjectContext queryEntityForName:#"Person" predicateFormat:nil argumentArray:nil];

How to store array of NSManagedObjects in an NSManagedObject

I am loading my app with a property list of data from a web site. This property list file contains an NSArray of NSDictionaries which itself contains an NSArray of NSDictionaries. Basically, I'm trying to load a tableView of restaurant menu categories each of which contains menu items.
My property list file is fine. I am able to load the file and loop through the nodes structure creating NSEntityDescriptions and am able to save to Core Data. Everything works fine and expectedly except that in my menu category managed object, I have an NSArray of menu items for that category. Later on, when I fetch the categories, the pointers to the menu items in a category is lost and I get all the menu items. Am I suppose to be using predicates or does Core Data keep track of my object graph for me?
Can anyone look at how I am loading Core Data and point out the flaw in my logic? I'm pretty good with either SQL and OOP by themselves, but am a little bewildered by ORM. I thought that I should just be able to use aggregation in my managed objects and that the framework would keep track of the pointers for me, but apparently not.
NSError *error;
NSURL *url = [NSURL URLWithString:#"http://foo.com"];
NSArray *categories = [[NSArray alloc] initWithContentsOfURL:url];
NSMutableArray *menuCategories = [[NSMutableArray alloc] init];
for (int i=0; i<[categories count]; i++){
MenuCategory *menuCategory = [NSEntityDescription
insertNewObjectForEntityForName:#"MenuCategory"
inManagedObjectContext:[self managedObjectContext]];
NSDictionary *category = [categories objectAtIndex:i];
menuCategory.name = [category objectForKey:#"name"];
NSArray *items = [category objectForKey:#"items"];
NSMutableArray *menuItems = [[NSMutableArray alloc] init];
for (int j=0; j<[items count]; j++){
MenuItem *menuItem = [NSEntityDescription
insertNewObjectForEntityForName:#"MenuItem"
inManagedObjectContext:[self managedObjectContext]];
NSDictionary *item = [items objectAtIndex:j];
menuItem.name = [item objectForKey:#"name"];
menuItem.price = [item objectForKey:#"price"];
menuItem.image = [item objectForKey:#"image"];
menuItem.details = [item objectForKey:#"details"];
[menuItems addObject:menuItem];
}
[menuCategory setValue:menuItems forKey:#"menuItems"];
[menuCategories addObject:menuCategory];
[menuItems release];
}
if (![[self managedObjectContext] save:&error]) {
NSLog(#"An error occurred: %#", [error localizedDescription]);
}
You set a NSArray as to-many relationship object
NSMutableArray *menuItems = [[NSMutableArray alloc] init];
[menuCategory setValue:menuItems forKey:#"menuItems"];
which might cause the trouble.(should throw an exception?) Relationships in CoreData are always unsorted, therefore NSSets. Add a sortIndex property to your entities for ordering.
I had the same issue. There are 2 major problems with using NSSets and Core Data: if you need non-distinct objects and need them ordered. As an example, say you have 2 entities in Core Data: professor and student. The student takes 10 classes for a degree program and you wish to have a (one-to-many) relationship from the student to the professor in order that the classes were taken. Also, the same professor may teach more than one class. This was how I overcame the issue. Create a Binary Data attribute (we'll call it profData) in student and store dictionaries that make it possible to reconstruct the data as needed. Note: don't store an array of professors, since they inherit from NSManagedObject vs. NSObject. That can cause problems. You can bolt on the required methods using a category. In this example, I created a category on Student called ProfList (Student+ProfList.h/m). This keeps the code out of the NSManagedObject subclasses, so if my attributes in Core Data change, I can regenerate the subclasses automatically without wiping out this code. Here is some sample code:
// Student+ProfList.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "Student.h"
#import "Professor.h"
#interface Student (ProfList)
- (NSArray *)getStudentsFullList;
- (void)storeStudentsFullList:(NSArray *)fullList;
#end
// Student+ProfList.m
#import "Student+ProfList.h"
#implementation Student (ProfList)
- (NSArray *)getStudentsFullList
{
NSData *storedData = self.profData;
if (!storedData) return nil;
NSMutableArray *fullList = [[NSMutableArray alloc] init];
// Retrieve any existing data
NSArray *arrayOfDictionaries = [NSKeyedUnarchiver unarchiveObjectWithData:storedData];
// Get the full professor list to pull from when recreating object array
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Professor"];
NSSortDescriptor *alphaSort =
[[NSSortDescriptor alloc] initWithKey:#"name"
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)];
[fetchRequest setSortDescriptors:#[alphaSort]];
NSSet *allProfessors = [NSSet setWithArray:[context executeFetchRequest:fetchRequest error:nil]];
for (NSDictionary *dict in arrayOfDictionaries) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name LIKE %#", [dict objectForKey:#"name"]];
NSSet *filteredSet = [allProfessors filteredSetUsingPredicate:predicate];
Professor *newProfessor = [filteredSet anyObject];
newProfessor.index = [dict objectForKey:#"index"];
[fullList addObject:newProfessor];
}
return fullList;
}
- (void)storeStudentsFullList:(NSArray *)fullList
{
NSMutableArray *encodedList = [[NSMutableArray alloc] init];
for (Professor *professor in fullList) {
[encodedList addObject:#{#"index" : #([encodedList count]), #"name" : professor.name}];
}
NSArray *encodedArray = [NSArray arrayWithArray:encodedList];
NSData *arrayData = [NSKeyedArchiver archivedDataWithRootObject:encodedArray];
self.profData = arrayData;
}
#pragma mark - Core Data
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
#end
You store a local variable in a view controller, then send this message to the student instance to get the list and save it locally for use in a table view or whatever.