Core data one-to-many relationships - ios

I've been searching around the internet for days looking for tutorials on how to work with two entities and adding values for the data and linking them. Here is how my app is set up:
I'm making an app that allows the user to create an athlete and within that athlete they can add multiple sporting events. I have two entities: Athletes and Events with a to-one relation from Athletes to Events and a to-many relationship inversely.
The issue I'm having is what code to write to add the name and opponent attribute values within the Events entitiy, all while making sure that that specific name and opponent match up to only one athlete. I've tried using Core Data Accessor Methods, as well as creating new NSManagedObjects for either entitity and adding the values for specific keys.
I've tried to follow the CoreDataRecipes sample code, as well as the common Core Data tutorials on the web. Can anyone help steer me in the right path with some basic method functions or other tutorials that helped you? Thanks.

You need to stop thinking in terms of RDBMS (relational databases) and start thinking in terms of managed object models. CoreData deals with managing objects and their associations. You can associate Athlete objects to Event objects (by the way I suggest using singular vs. plural for entity names i.e. Athlete and Event vs. Athletes and Events and use singular for to-one relationships and plural for to-many relationships). It's a preference thing but I refer to myself as an Athlete (object) not an Athletes (objects). Makes it all more readable and intuitive as well.
Assuming your entities look like this given how you described the relationships:
#interface Athlete : NSManagedObject
#property (nonatomic, retain) NSString * name;
//... a bunch more attributes
#property (nonatomic, retain) Event *event; // use singluar for relationship name too
//...
#end
#interface Event : NSManagedObject
#property (nonatomic, retain) NSString * eventName;
//... a bunch more attributes
#property (nonatomic, retain) NSSet *athletes; // use plural for relationship name
//...
#end
#implementation MyViewController
//... some method
// fetch the athletes (possibly present in table view or other mechanism for selection)
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Athlete"];
NSArray *athletes = [managedObjectContext executeFetchRequest:fetchRequest error:nil];
// select the althletes (primary & opponent - hardcoded for example)
//...
NSArray *selectedAthletes = [NSArray arrayWithObjects: athletes[0], athletes[1], nil];
// create an event
Event *event = [NSEntityDescription
insertNewObjectForEntityForName:#"Event"
inManagedObjectContext:context];
// add the athletes
[event addAthletes:[NSSet setWithArray:selectedAthletes]];
//...
#end
Now you have an event with the 2 athletes. If you wanted to distinguish between the opponent and challenger then you could create 2 to-one relationships (from Event to Athlete) such as opponent and challenger and associate the Event to the Athlete via something like:
#interface Event : NSManagedObject
#property (nonatomic, retain) NSString * eventName;
//... a bunch more attributes
#property (nonatomic, retain) Athlete *opponent; // use singluar for relationship name
#property (nonatomic, retain) Athlete *challenger;
//...
#end
//...
event.opponent = athletes[0];
event.challenger = athletes[1];
//...

Related

ios/xcode/coredata: Data model for many to many relationships

IOS newb from mysql background working on an app that ties into a web server backend. If I have two entities or objects enjoying many to many relationships such as item and tag, in MYSQL I would have three tables, the table of items, the table of tags and a third table of tag-item relationships.
Table 1 Tags
ID|tag
Table 2 Items
ID|item
Table 3 Tagitems
ID|tagid|itemid
If I want to do this in core data would it be appropriate to also have three entities?
Entity 1: Tags
id|tagname
Entity 2: Items
id|itemname
Entity 3: Tagitems
id|tagid|itemid
Seems straightforward enough but just want to make sure I am understanding core data correctly.
If your intermediate table has no other attributes, then you do not need to model it yourself. Just create a to-many relationship from Entity 1 to Entity 2, and a to-many relationship from Entity 2 to Entity 1, and make each relationship the inverse of the other. CoreData will build and manage the intermediate table for you (and its existence is largely hidden from you). The model editor should look something like this:
When you generate subclasses, CoreData will create NSSet properties for the relationships:
Tag.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class Item;
#interface Tag : NSManagedObject
#property (nonatomic, retain) NSString * tagName;
#property (nonatomic, retain) NSSet *items;
#end
#interface Tag (CoreDataGeneratedAccessors)
- (void)addItemsObject:(Item *)value;
- (void)removeItemsObject:(Item *)value;
- (void)addItems:(NSSet *)values;
- (void)removeItems:(NSSet *)values;
#end
Item.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class Tag;
#interface Item : NSManagedObject
#property (nonatomic, retain) NSString * itemName;
#property (nonatomic, retain) NSSet *tags;
#end
#interface Item (CoreDataGeneratedAccessors)
- (void)addTagsObject:(Tag *)value;
- (void)removeTagsObject:(Tag *)value;
- (void)addTags:(NSSet *)values;
- (void)removeTags:(NSSet *)values;
#end
You can then use the core data generated accessors to add and remove relationships - note that you set the relationships directly between objects, you don't have to use IDs; CoreData is handling the IDs for you "in the shadows". So you could do something like this:
Tag *actionTag = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:self.context];
actionTag.name = #"Action";
Tag *dramaTag = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:self.context];
dramaTag.name = #"Drama";
Item *movie = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:self.context];
movie.name = #"Pride and Prejudice";
[movie addTagsObject:dramaTag];
If your intermediate table does have other attributes, then you should implement Entity 3, and add whatever attributes you need (but do not implement the 'id' keys - leave that to CoreData). The relationships from Entity 1 to Entity 3 should be to-many, but its inverse should be to-one; likewise from Entity 2 to Entity 3 should be to-many, and its inverse to-one:
Entity 2 <---->> Entity 3 <<----> Entity 1

CoreData and the most common word used/tagged

I have an entry field similar to the "tags" feature on this site: user enters bunch of words or phrases that are shown in their own bubbles. These words/phrases are associated with a journal entry (basically tagging the journal entry) and on the overview page I would like to show top 5 or 10 of these words/phrases.
My initial implementation is to store a string in each journal entry that basically concatenates all of these words/phrases. When I read the data, I split this string based on predefined separator. That works pretty well, but finding top 5/10 will be super bad if I have a lot of entries that contain decent amount of these words/phrases. What would be a better approach to this?
So, i have something like this:
#interface JournalEntry : NSManagedObject
{
#property (nonatomic, retain) NSNumber * entryId;
#property (nonatomic, retain) NSDate * dateCreated;
#property (nonatomic, retain) TagsInfo *tagsInfo;
}
#interface TagsInfo : NSManagedObject
{
#property (nonatomic, retain) JournalEntry * journalEntry;
#property (nonatomic, retain) NSString * tagString;
}
I think in a normal database setup, I would create a table for tags, where I would store something like [journalEntryId, tagEntry]. There would be a bunch of these entries for each journal entry. Should this be similiar?
That's what I'd do, create a new Tag entity that has as many-to-many relationship to JournalEntry. Migrate the existing TagInfo to Tag and update the relationships.
Tag would have the following, at a minimum:
To-many relationship to JournalEntry
String called tagString (or tagName or whatever).
JournalEntry would have a to-many relationship back to Tag. The tagString would be unique, since each Tag could relate to multiple journal entries.
You could then fetch the tags along with a count of how many times each was used like this:
NSExpression *tagCountExpression = [NSExpression expressionWithFormat:#"count(journalEntries)"];
NSExpressionDescription *tagCountExprDescription = [[NSExpressionDescription alloc] init];
tagCountExprDescription.name = #"count";
tagCountExprDescription.expression = tagCountExpression;
tagCountExprDescription.expressionResultType = NSInteger64AttributeType;
NSFetchRequest *tagFetch = [NSFetchRequest fetchRequestWithEntityName:#"Tag"];
[tagFetch setResultType:NSDictionaryResultType];
tagFetch.propertiesToFetch = #[ #"tagString", tagCountExprDescription ];
tagFetch.propertiesToGroupBy = #[ #"tagString" ];
That would give you an array of dictionaries. Each would contain a tagString and a count of how many related journal entries that tag has. You'd have to sort it yourself (can't use expressions as sort descriptors).

Get NSEntityDescription from to-many relationship

I have a user entity with a to-many relationship called "recordings".
The generated interface looks like this:
#interface User : NSManagedObject
#property (nonatomic, retain) NSString * display_name;
// ...
#property (nonatomic, retain) NSSet *recordings; // <-- this one
#end
#interface User (CoreDataGeneratedAccessors)
- (void)addRecordingsObject:(Recording *)value;
- (void)removeRecordingsObject:(Recording *)value;
- (void)addRecordings:(NSSet *)values;
- (void)removeRecordings:(NSSet *)values;
#end
I'm keeping this user inside a session object (singleton) which is used throughout the application. To add a recording, I've created a convenience method that will return an empty "Recording" object which can then be populated by the calling code:
- (Recording *)newRecording
{
Recording *recording = [NSEntityDescription
insertNewObjectForEntityForName:#"Recording"
inManagedObjectContext:self.managedObjectContext];
return recording;
}
I pass the #"Recording" entity name to insertNewObjectForEntityForName:: and this works fine; however, I don't like that my object knows the actual entity name for its .recordings relationship property.
Is there an easy way in which I can say "Create a new object for this relationship"?
You could look at mogenerator and the convenience methods it adds which allow you to ask an NSManagedObject subclass for its entity name. This would be the minimal change to your current code.
Otherwise, you'd be using the entity description to get the relationshipsByName, using the associated relationship description to get the destinationEntity and then you have the name that you need. You could add this as a category method so it's easily accessible.
If you don't want to use mogenerator, as #Wain suggested, you can mirror its approach. Write a class method on Recording to create a new Recording that is related to a particular User.
In Recording.h:
+ (Recording *)insertInManagedObjectContext:(NSManagedObjectContext *)moc;
+ (Recording *)insertWithUser:(User *)user;
In Recording.m:
+ (Recording *)insertInManagedObjectContext:(NSManagedObjectContext *)moc
{
Recording *recording = [NSEntityDescription
insertNewObjectForEntityForName:#"Recording"
inManagedObjectContext:moc];
return recording;
}
+ (Recording *)insertWithUser:(User *)user
{
// an NSManagedObject subclass knows its MOC
Recording *recording = [self insertInManagedObjectContext:user.managedObjectContext];
[recording.users addRecordingsObject:recording];
return recording;
}
You'll also want to write a method to search for an existing Recording object using whatever uniquing criteria are appropriate (the "find-or-create" pattern), and invoke that method within +insertWithUser: instead of always inserting a new Recording.

CoreData transient relationship example

Does anybody have an example on how to model and code a transient to-one relationship in CoreData? For example, I have 2 entities with a one-to-many relationship. Doctor and Appointment. Now I want an transient relationship called mostRecentAppointment on the doctor entity. It's straightforward to model in the xcode designer, but I'm not sure about the implementation side. Also should I implement an inverse? Seems silly.
Have a look at this code I wrote recently, to cache an image in an NSManagedObject:
First you define a transient property in your model (notice that if your transient property points to an object type other than those supported by CoreData you'll leave as "Undefined" in the model)
Then, you re-generate your NSManagedObject subclass for that entity or just add the new property manually, the header file should look like this:
#interface Card : NSManagedObject
#property (nonatomic, retain) NSString * imagePath;
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSNumber * order;
#property (nonatomic, retain) NSString * displayName;
#property (nonatomic, retain) UIImage *displayImage;
#end
Here we change the class of the transient property to the actual class type
e.g. displayImage type here is UIImage.
In the implementation file (or an extension class) you implement the getter/setter for your transient property:
-(UIImage*)displayImage{
//Get Value
[self willAccessValueForKey:#"displayImage"];
UIImage *img = (UIImage*)[self primitiveValueForKey:#"displayImage"];
[self didAccessValueForKey:#"displayImage"];
if (img == nil) {
if ([self imagePath]) { //That is a non-transient property on the object
img = [UIImage imageWithContentsOfFile:self.imagePath];
//Set Value
[self setPrimitiveValue:img forKey:#"displayImage"];
}
}
return img;
}
Hope that helps you.
What you need to do is add an entity of type Appointment called newAppointment and set this each time you create a new appointment for a given doctor. Its that simple.
Always implement an inverse as apple recommend this for validation and core data efficiency.
Alternatively you could timestamp the appointments and use NSPredicates to search for the latest appointment in a given Doctor's linked appointments.
In this case, the appropriate method to override is -awakeFromFetch in the Doctor entity, for example like so:
- (void)awakeFromFetch {
[super awakeFromFetch];// important: call this first!
self.mostRecentAppointment = <something>; // normal relationship
self.mostRecentAppointment.doctor = self; // inverse relationship
}
In the model designer, mark both the normal and the inverse relationship as transient. That should be it.
Well, you'll just have to try out, in your own sample program that can be no more than an hour to set up correctly.
My guess is --- no extra coding will be needed. If Apple's documentation on CoreData is correct, the only difference between a normal attribute/relationship and a "transient" one is that the latter is not persisted, meaning, when you "save" it does not update the persistent-store.
I would guess that otherwise all the aspects of it are complete, together with KVO/KVC compliance, Undo support, validation, and automatic update by delete rules. The only thing is that after a fresh Fetch of the entity --- the transient relationship will always be nil.
For that --- I would of course NOT RECOMMEND setting up a transient relationship as "non-optional", because it is very likely to be null most of the time for most of the entities.
I would set up a reverse relationship (transient as well and named wisely) and have both delete rules be "Nullify".
So far is for transient relation.
But here is an alternative I came up with, trying to solve almost-the-same problem. My "appointment" is one of the related appointments, but not just the "latest", but the first "unfinished" one. Very similar logic.
Instead of a transient relationship, I added a new calculated property to my "Doctor" entitys generated NSManagedObject subclass, in a category, like this:
#interface XXDoctor (XXExtensions)
/**
#brief Needs manual KVO triggering as it is dependent on a collection.
Alternatively, you can observe insertions and deletions of the appointments, and trigger KVO on this propertyOtherwise it can be auto-
#return the latest of the to-many appointments relation.
**/
#property (readonly) XXAppointment *latestAppointment; // defined as the
#end
Implementation:
#import "XXDoctor".h"
#import "XXAppointment.h"
#implementation XXDoctor (XXExtensions)
// this won't work because "appointments" is a to-many relation.
//+ (NSSet *)keyPathsForValuesAffectingLatestAppointment {
// return [NSSet setWithObjects:#"appointments", nil];
//}
- (XXAppointment *) latestAppointment {
NSInteger latestAppointmentIndex = [self.appointments indexOfObjectPassingTest:^BOOL(XXAppointment *appointment, NSUInteger idx, BOOL *stop) {
*stop = (appointment.dateFinished == nil);
return *stop;
}];
return (latestAppointmentIndex == NSNotFound) ? nil : [self.appointments objectAtIndex: latestAppointmentIndex];
}
#end

Managed Object in Core Data entity causing "is not key value coding-compliant for key" error

I'm pretty new to Core Data, and objective-c. I have been up and down the Core Data documentation, and don't know what I'm doing wrong here.
At runtime, I'm getting the following error when adding the NSManagedObject "ReportItem" to the NSMutableSet "reports" in the NSManagedObject "RoomItem": NSUnknownKeyException', reason: '[<RoomItem 0x747c850> valueForUndefinedKey:]: the entity RoomItem is not key value coding-compliant for the key "reports"
The error is occurring inside of the "RoomList" store class in the "creatReportForRoom" method below, which is called upon viewWillDisapear in the UIViewController where the relevant data is entered:
- (ReportItem *)creatReportForRoom:(RoomItem *)currentRoom Report:(ReportItem *)report
{
NSDate *dateCreated = [NSDate date];
ReportItem *detailItem = [NSEntityDescription insertNewObjectForEntityForName:#"ReportItem" inManagedObjectContext:context];
[detailItem setDateReportCreated:dateCreated];
NSMutableSet *reports = [currentRoom mutableSetValueForKey:#"reports"];
[reports addObject:detailItem]; //error is occurring at this line
[allReports addObject:detailItem];
return detailItem;
}
The "RoomItem" NSManagedObject files are here:
RoomItem.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#interface RoomItem : NSManagedObject
#property (nonatomic, strong) NSString *building;
#property (nonatomic, strong) NSString *room;
#property (nonatomic, retain) NSString *imageKey;
#property (nonatomic, retain) UIImage *buildingImage;
#property (nonatomic, strong) UIImage *buildingThumbnail;
#property (nonatomic, strong) NSData *buildingThumbnailData;
#property (nonatomic) double objectIndex;
#property (nonatomic, strong) NSDate *dateCreated;
#property (nonatomic, retain) NSMutableSet *reports;
#end
RoomItem.m
#import "RoomItem.h"
#implementation RoomItem
#dynamic building, buildingThumbnail, buildingThumbnailData, objectIndex, room, dateCreated, imageKey, buildingImage, reports;
#end
I've including the "createItem" method were the RoomItem is created and stored, in case I'm doing something wrong there:
- (RoomItem *)createItem
{
double order;
//create new roomItem
//tracks what number item it's creating
if ([allItems count] == 0) {
order = 1.0;
}
else
{
order = [[allItems lastObject] objectIndex] + 1;
}
NSLog(#"Adding after %d items, order = %.2f", [allItems count], order);
RoomItem *detailItem = [NSEntityDescription insertNewObjectForEntityForName:#"RoomItem"
inManagedObjectContext:context];
[detailItem setObjectIndex:order];
[detailItem setDateCreated:[NSDate date]];
[allItems addObject:detailItem];
return detailItem;
}
Data Model with relationships:
Relationship Diagram:
Any advice on fixing this problem would be hugely appreciated. Please let me know if there's anything I failed to include that might help.
Thanks!
There is a discrepancy between the Core Data model shown in your image and your code.
In the model image, reports is a relationship from ReportItem to RoomItem.
Your code treats it as a relationship from RoomItem to ReportItem.
If the latter is what you want, then you have to name the relationships in the Core Data model editor correctly: reports as a relationship on RoomItem and roomReportsFor as the inverse relationship on ReportItem.
I also strongly recommend that you don't write the managed object subclass files yourself, but let it generate by Xcode: Select the entities in the Core Data model editor, and choose "Editor -> Create NSManagedObject Subclass ..." from the menu.
There is another error in your "RoomItem.h" file: The data type for to-many relationships is NSSet, not NSMutableSet.
Using the Xcode created managed object subclass files helps to avoid such inconsistencies and errors. You only have to remember to re-created the files after changes in the Core Data model.
(Just for the sake of completeness: There are also other tools to create the managed object subclasses, such as mogenerator, which some people recommend.)
This is a simplified version of what I think the relationships should look like:
reports: from RoomItem to ReportItem (to-many relationship),
room: from ReportItem to RoomItem (to-one relationship, inverse of reports).
I have experienced this as well when updating the Core Data model. In some cases it seems as the simulator holds on to the old model. This can usually be fixed by deleting the app from the simulator and clean/build.

Resources