CoreData transient relationship example - ios

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

Related

Handling undefined keys in Core Data

Is it possible to set a handler for undefined keys in Core Data?
I'm asking because despite defining valueForUndefinedKey: my implementation of that method is never called if valueForKey: is invoked on a managed object that doesn't have an attribute with that key.
This is needed for a synchronization system I'm currently writing where an object can be marked as locallyCreated or locallyDeleted but at the same time not all objects are editable so I want to avoid defining these properties for all entities in my model (around ~25 entities).
Although it seems tempting to create a single parent entity for that purpose I would like to avoid doing that since that will put all objects in one giant SQLite table which as far as I know will have negative impact on performance.
Currently I have a base "entity" class called RemoteObject that defines some common attributes like remoteID, locallyCreated, locallyDeleted, as suggested in another answer on SO, which all other entities inherit in code like this:
#interface RemoteObject : NSManagedObject
#property (nonatomic) NSString *remoteID;
#property (nonatomic) BOOL locallyCreated;
#property (nonatomic) BOOL locallyDeleted;
#end
#implementation RemoteObject
#dynamic remoteID;
#dynamic locallyCreated;
#dynamic locallyDeleted;
#end
#interface Project : RemoteObject
// custom properties
#end
What I want is to inspect any given RemoteObjet and see if it was locally create or deleted. However, as I said above, not all of the entities have corresponding attributes, so Core Data will throw an exception.
I found a workaround that allows me to avoid those errors - define a class method instead:
- (id)valueForKeyIfExists:(NSString *)key {
if (self.entity.attributesByName[key] != nil) {
return [self valueForKey:key];
}
return nil;
}
+ (BOOL)objectIsLocallyCreated:(RemoteObject *)object {
return [[object valueForKeyIfExists:#"locallyCreated"] boolValue];
}
But I was wondering if it would be possible to refactor this into object properties instead, catching undefined keys with valueForUndefinedKey: like this:
- (id)valueForUndefinedKey:(NSString *)key {
if ([key isEqualToString:LocallyCreatedKey]
|| [key isEqualToString:LocallyDeletedKey]) {
return #(NO);
}
return [super valueForUndefinedKey:key];
}
- (BOOL)locallyDeleted {
return [[self valueForKey:LocallyDeletedKey] boolValue];
}
It would be better to move those 2 flag attributes into a different class and then anything which doesn't have them is a subclass of RemoteObject and anything which does is a subclass of the new class, perhaps TrackedRemoteObject. Then in your algorithm you can class test to determine conformance.

Why NSManagedObject Subclass can't use class_copyMethodList find dynamic generated method?

When I create a NSManagedObject Subclass Employee,it has a property nameaccording the EntityDescription in xcdatamodelfile. And in the .m file, the code modify it using #dynamic like this:
#interface Employee (CoreDataProperties)
#property (nullable, nonatomic, retain) NSString *name;
#end
#implementation Employee (CoreDataProperties)
#dynamic name;
#end
According to the Apple's Document:
Core Data dynamically generates efficient public and primitive get and set attribute accessor methods and relationship accessor methods for properties that are defined in the entity of a managed object’s corresponding managed object model. Therefore, you typically don’t need to write custom accessor methods for modeled properties.
According this, I think the CoreData Framework will create two method named name and setName:in the runtime. So I use such code to verify my thinking.
Employee *object = [NSEntityDescription insertNewObjectForEntityForName:#"Employee" inManagedObjectContext:self.managedObjectContext];
object.name = #"1";
[[self class] showInstanceMethod:[Employee class]];
+ (void)showInstanceMethod:(Class)class {
unsigned int outCount;
//..show InstanceMethodList
Method *methodsList = class_copyMethodList(class, &outCount);
for (int i = 0; i < outCount; i ++) {
SEL sel = method_getName(*methodsList);
NSString *methodName = NSStringFromSelector(sel);
NSLog(#"\nmethodName:%#\n", methodName);
methodsList++;
}
}
I'm sad it didn't log any method name like name or setName:.But I use this code object.name = #"1"; and didn't have any problem.
When they say "dynamically" they really do mean it - the dynamic implementation seems to be provided only as and when the selector is called (directly or via valueForKey:). You can see this happening if you override resolveInstanceMethod: in your Employee class. Call the super implementation and log the selector name and return value. Presumably the method will be listed by class_copyMethodList at this point, though I've never checked.

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.

Core data one-to-many relationships

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];
//...

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