Suppose I have an entity MeetingNote with standard attributes plus a one to many relationship to another entity Tag.
Out of MeetingNote instances, I want to create instances of another entity called Task but only in case meetingNote.tag.name == 'task' is TRUE.
What do you think would be the correct way of modelling the relationship between Task and MeetingNote? Or should there be a relationship at all and I ought to use a fetched property with the appropriate predicate instead?
Firstly, the true purpose of Core Data is not persistence but rather to create the model layer of a Model-View-Controller design app. That means that Core Data is really a model/simulation API first and a persistence API second. A Core Data data model, therefore, should accurately represent the attributes of real-world objects, conditions or events and the relationships between them.
So, when you set down to build a data model, you should forget about the UI, the data source or any other implementation details and simply try to create a model that mirrors the real-world objects, conditions or events the app deals with.
Secondly, while a data model deals with how entities are related, it doesn't deal with the logic of why the entities are related. That logic is belongs in code somewhere often in the custom NSManagedObject subclasses for the entities. In this case, the how of the entities relationships is that the MeetingNote entity is related to both Task and Tags. The why is that there should be a relationship between any particular MeetingNote object and anyTask object only if the MeetingNote object has a relationship to a Tag object with the name of task.
So, your basic data model should look like this:
MeetingNote{
title:string
date:date
tags<<-->>Tag.meetingNotes
tasks<-->>Task.meetingNote
}
Task{
name:string
meetingNote<<-->MeetingNote.tasks
}
Tag{
name:string
meetingNotes<<-->>MeetingNote.tags
}
Now the question becomes one of where to stick the custom logic for the why. The most logically simple way would be to create a custom accessor for MeetingNote.tags property that checks if name of a tag being added or removed to an MeetingNote instance equals task and if so, adding or removing a Task object from the instance's MeetingNote.tasks relationship.
However, that has an obvious performance penalty of having to check every tag added or removed. A better solution would be to add the custom to only one point that is called only when the exact condition of MeetingNote.tags.name' contains a value oftask`.
Let's assume you have the following constraints:
A MeetingNote object cannot have a related Task object without also having a Tag object with name=="task".
If the MeetingNote object does have a Tag object with name=="task" it must have at least one related Task object.
If a MeetingNote object looses its relationship to a Tag object with name=="task", then it loses all its task.
It is immediately obvious at this point that `Tag object with name=="task" is a special object with behaviors different from other tags. This justifies and requires that it have its own entity and subclass so we would add to the data model:
TaskTag:Tag{
}
Since the TaskTag entity inherits from the Tag entity it can automatically inherits the in the Tag.meetingNotes relationship so it will behave as a Tag object from the perspective of any MeetinNote objects.
Then in the TaskTag NSManagedObject subclass we would add the following code:
-(NSString *) name {
// the name of a TaskTag is always "task"
// you should set the defalut value in the data model to "task" as well.
return #"task";
}
-(void) setName:(NSString *)name{
return; // the name can never be changed
}
- (void)addMeetingNotesObject:(MeetingNote *)value {
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
[self willChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[[self primitiveValueForKey:#"meetingNotes"] addObject:value];
// If the meeting object does not an existing task, add one
if ([value.tasks count]==0 ) {
Task *t=[NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:self.managedObjectContext];
t.meetingNote=value;
}
[self didChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[changedObjects release];
}
- (void)removeMeetingNotesObject:(MeetingNote *)value {
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
[self willChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueMinusSetMutation usingObjects:changedObjects];
[[self primitiveValueForKey:#"meetingNotes"] removeObject:value];
// A MeetingNote object cannot have any task without a taskTag so remove all task objects
if ([value.tasks count]!=0 ) {
[value removeTasks:value.tasks]; // removes all tasks from meeting notes
}
[self didChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueMinusSetMutation usingObjects:changedObjects];
[changedObjects release];
}
- (void)addMeetingNotes:(NSSet *)value {
[self willChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueUnionSetMutation usingObjects:value];
[[self primitiveValueForKey:#"meetingNotes"] unionSet:value];
Task *newTask;
// same as addMeetingNotesObject:
for (MeetingNote *meetNote in value) {
if ([meetNote.tasks count]==0 ) {
newTask=[NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:self.managedObjectContext];
newTask.meetingNote=value;
}
}
[self didChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueUnionSetMutation usingObjects:value];
}
- (void)removeMeetingNotes:(NSSet *)value {
[self willChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueMinusSetMutation usingObjects:value];
[[self primitiveValueForKey:#"meetingNotes"] minusSet:value];
//removeMeetingNotesObject:
for (MeetingNote *meetNote in value) {
[meetNote removeTasks:meetNote.tasks];
}
[self didChangeValueForKey:#"meetingNotes" withSetMutation:NSKeyValueMinusSetMutation usingObjects:value];
}
// Note: This code is not compiled and my contain errors.
This code will automatically enforce the constraints above without having to do anything else. You could also customize the Task subclass to set its name automatically based on some attributes of the MeetingNote object it is related to.
Now you have all the why logic in the data model and your constraints are automatically enforced. This may not be the exact solution you need but you get the idea.
Fun question. My experience here is limited, but I couldn’t resist attempting an answer:
If you expect a lot of edits to MeetingNote that need to be immediately reflected in Task, or vice versa, a relationship would automatically keep the loaded objects mutually updated. (I base this on Richard Stahl’s post here: fetched properties vs. relationships.) Otherwise the fetched property might be more efficient faulting-wise.
But why are you doing the relationship between MeetingNote and Tag as one-to-many? That means a tag can have only one MeetingNote. Right? So any time a meetingNote gets tagged “task,” a separate tag has to be created. Wouldn’t many-to-many be better?
And then, if going the relationship route, you would do a one-to-one relationship between MeetingNote and Task. Even if you expected multiple meetingNotes to share a task, since the task has to be derived directly from the meetingNote, you're going to be creating separate tasks anyway. And since you're creating one task instance per task-tagged meetingNote, those tasks shouldn't have multiple meetingNote relationships because that would create confusing duplication.
Related
I have a to-many relationship, e.g. A student has many classes.
Why is it when I run the following code _classes does NOT have the new Cls object in it? Yet when I close the app and reopen it does.
And as a follow up question, How would I get the new list of classes right after adding adding a new one?
NSMutableSet* classes = [student mutableSetValueForKey:#"classes"];
NSManagedObject* cls = [NSEntityDescription insertNewObjectForEntityForName:#"Cls" inManagedObjectContext:context];
[cls setValue:name forKey:#"name"];
[student addClassesObject:cls];
NSError* err;
[context save:&err];
NSMutableSet* _classes = [student mutableSetValueForKey:#"classes"];
And this is what my addClassesObject: operation looks like (auto generated by XCode)
- (void)addClassesObject:(Cls*)cls
{
}
Thank you!
You can't add objects to the set directly. You need to use Core Data generated accessors or your own custom ones. Look in your .h file and you should see something along the lines of
-(void)addClsObject:(Cls *)theObject;
And
-(void)addClsObjects:(NSSet *)set;
So for your particular case, you would do the following once you have your Cls object:
[student addClassesObject:cls];
Then perform you save and you should be able to retrieve the classes set. FYI, calling this method and the relatd NSSet relatd method also sets up the reverse relationship for you.
Good luck
T
My data model contains two entities: Author and Book with a one to many relationship (one author may write several books).
Let's say that there are only two books and two authors in DB as follows:
Book A is assigned to Author X
Book B is assigned to Author Y
Assuming following change is applied:
Book B is assigned to a new Author Z.
Result:
Author Y exists in DB but points to no book.
My question: is it possible to configure the data model so objects like Author Y will be automatically deleted when they are not referenced by any book?
Check out "delete propagation". It's there to solve exactly that problem.
If that doesn't do exactly what you want / need: You can override - (void)prepareForDeletion on the Book entity and at that point check for any Authors that are registered with the context and have pending changes (since their inverse will have changed) and have no books:
{
// ...
[[NSNotificationCenter defaultNotificationCenter] addObserver:self selector:#selector(deleteOrphanedAuthors:) name:NSManagedObjectContext object:moc];
// ...
}
- (void)deleteOrphanedAuthors:(NSNotification *)note;
{
NSManagedObjectContext *moc = [note object];
NSManagedObjectModel *mom = [[moc persistentStoreCoordinator] managedObjectModel];
NSEntityDescription *authorEntity = [[mom entitiesByName] objectForKey:#"Author"];
for (NSManagedObject *author in [moc updatedObjects]) {
if ([author entity] == authorEntity) {
if (![author hasFaultForRelationshipNamed:#"books"] && ([[author books] count] == 0)) {
[moc deleteObject:author];
}
}
}
}
Note: You can not pass nil as the object (i.e. context) to observe, since frameworks you use, might have their own context, and you do not want to mess with them.
Also, note how this code is careful not to touch the author object if it's a fault. If a book is deleted, Core Data will change the corresponding author objects' inverse relationships, hence fault in that relationship, such that it is no longer a fault. And the code will only operate on those objects.
You will need to determine "orphaned" books manually.
When you update the Author relationship you could check the old Author's books relationship to see if it still has any books.
Alternatively you could use notifications to determine when the NSManagedObjectContext changes: NSManagedObjectContextObjectsDidChangeNotification. If you register for this notification you can check for a number of changes to Author objects. Have a look at that specific notification in the docs.
I have a super entity in core-data called element and has two sub entity called (IsBoolean,IsGrade) i try to access these sub-entity attribute from below code.I need your help about this issue
--------------------- Core data structure --------------------
Super Entity [Element->elmentID]
Sub Entities [IsBoolean->value] + [IsGrade->value]
---------------------- brief code ----------------------------
NSFetchRequest *formRequest = [[NSFetchRequest alloc]init];
NSEntityDescription *formEntity = [NSEntityDescription entityForName:#"Element" inManagedObjectContext:ManagedObjectContext];
NSSortDescriptor *formDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"elementID" ascending:YES];
NSPredicate *formPredicate = [NSPredicate predicateWithFormat:#"elementID==%d",2];
[formRequest setPredicate:formPredicate];
[formRequest setEntity:formEntity];
[formRequest setSortDescriptors:[NSArray arrayWithObject:formDescriptor]];
[formRequest setIncludesSubentities:YES];
resultController_= [[NSFetchedResultsController alloc] initWithFetchRequest:formRequest managedObjectContext:ManagedObjectContext sectionNameKeyPath:nil cacheName:nil ];
resultController_.delegate =self;
for (Element *elementData in resultController_) {
// I can access super entity attribute
NSlog(#"%#",elementData.elementID);
// Here i can't access sub entity attribute from super entity
NSLog(#"%#",elementData.value);
}
Find the following screenshot for datamodel
Please note that sub-entities do not have to be sub-classes. The class hierarchy and entity hierarchy do not have to match. In most cases it will make sense to have them match, but there are use cases where that's not the case. In fact, not having them match may give you a lot of flexibility.
You can have an entity book* with corresponding **Book class, and an entity author with a corresponding Author class. In this case they would not have a common super-entity. But the classes may very well have a common super-class that defines and implements e.g. #property NSString *name; and #property UIImage *image; and related methods.
Update based on amended question
You will not have any instances of your sub-entities unless you have added managed objects to your context that are of the sub-entity.
Each sub-entity is equivalent to a subclass of your Element entity - so it will have the element attributes, plus the new attributes you have defined in the sub-entity. It will also inherit in class terms if you have made custom NSManagedObject subclasses.
To talk about accessing the properties of a sub-entity from within a super entity is missing the point.
To add instances of the sub-entity, you would just insert entities as you would any other managed object, using the sub-entity name.
Original answer, 90% of which is still valid
You're not trying to access them from a super entity, you're accessing them from whatever is doing your fetch request. In that case, if you check that your returned objects are of the appropriate class or have the relevant attributes, then yes.
But from within a super-entity, no, that doesn't make any sense. It's the same as class inheritance - you couldn't access .text from within a UIView just because UILabel is a subclass of it. But you could have a list of objects that were UIViews, check if one of them was in fact a UILabel, then access it's .text property.
For a fetch request returning a mix of entities and sub-entities, you'd do something like
if ([elementData.entity.name isEqualToString:#"IsBoolean"]) // means it is the isBoolean sub-entity
NSLog(#"%#",elementData.value);
Slightly better, you would get the attributes dictionary:
NSDictionary *attributes = elementData.entity.attributesByName;
if ([attributes objectForKey:#"value"]) // Means there is an attribute called "value"
NSLog(#"%#",elementData.value);
With the latter case the dictionary is full of NSAttributeDescription objects which tell you what kind of attribute you are dealing with. See the various core data class references (NSManagedObject, NSEntityDescription and NSAttributeDescription).
I'm working with a subclass of NSManagedObject. Actually, it inherits from a class that inherits from a class that itself inherits from NSManagedObject (that shouldn't be a problem, right?).
The problem
After I make changes to the properties of the object, the object remembers the changes for its lifetime, but the changes are never saved to the database.
How Do I Know This?
I know this because:
when I restart the app, the changes I've made are lost.
telling the context to refresh the object – AFTER I've made changes to the object and told the context to save – sets the object's values back to their original state before I made the changes.
when running the app in the simulator, I can look at the sqlite database file in the Finder, and it's modified date isn't updated when I attempt to save the context.
Nothing is being written to the database!
Context
I'm using the auto-generated delegate methods to create the store coordinator and the context. Then I'm passing the context to the view controllers in their init methods, as recommended in the docs. The store is SQLite.
I am able to successfully insert objects into the database and read them. I can even make property changes to the newly inserted object and save it successfully. I simply don't seem to be able to update object properties when the object is pulled back out of the database.
The object is fetched from the store via a relationship from another object. After making changes to its properties, I call the context's save method. However, before doing so, I call the object's isUpdated method and the context's hasChanges method, and both return false. Shouldn't they return true since I've just made changes to the object's properties but haven't saved the context?
More
If I call the object's committedChanges method before saving the context, however, passing in the names of the properties that I've changed, I get back the correct values of the properties. I'm not sure what this means. I would have thought that this means that the object's new property values have been successfully saved, but clearly they are not saved.
I know that the result objects is registered with a context. If I call
[[result managedObjectContext] refreshObject:result mergeChanges:YES];
the object reverts back to the original property values. This means that the context is there and that it is the same context from which the record was fetched. And it means that the new property values are never written tot he database.
Some Code
Here's the code where I'm poking around with all of these things. There are other places in my code where I'm making property changes, but the changes are never saved.
- (IBAction)statusControlChanged:(UISegmentedControl *)control {
WCAAssessmentResult *result = [self currentResult];
/* printing the existing property values */
if (![result.complete boolValue]) NSLog(#"result is in progress!");
else if ([result.passed boolValue]) NSLog(#"result is passed!");
else NSLog(#"result is not passed!");
/* changing the property values */
switch (control.selectedSegmentIndex) {
case 0:
NSLog(#"setting incomplete");
result.complete = [NSNumber numberWithBool:NO];
break;
case 1:
NSLog(#"setting passed");
result.passed = [NSNumber numberWithBool:YES];
result.complete = [NSNumber numberWithBool:YES];
break;
case 2:
NSLog(#"setting failed");
result.passed = [NSNumber numberWithBool:NO];
result.complete = [NSNumber numberWithBool:YES];
break;
default:
break;
}
/* this method always returns an empty dictionary */
NSLog(#"%#", [result changedValues]);
/* this method returns the values that I just set */
NSLog(#"%#", [result committedValuesForKeys:[NSArray arrayWithObjects:#"complete", #"passed", nil]]);
/* isUpdated returns false */
if (![result isUpdated]) {
NSLog(#"result is not updated?! WTF!?!?");
}
/* hasChanges returns false */
if (![[result managedObjectContext] hasChanges]) {
NSLog(#"context has no changes!? WTF!?!?");
}
/* saving the context produces no error */
NSError *error = nil;
if (![[result managedObjectContext] save:&error]) {
NSLog(#"save failed");
NSLog(#"%#",[error description]);
}
}
A Twist
If I create a new result object by inserting a new record into the context, I can set that object's properties and they are saved successfully. In the above code, I'm fetching the object as a member of a to-many association from another object. Is that a clue?
I'm tearing my hair out over this. What the hell could be going wrong here?
What's NOT The Problem
I've logged the object's class, and it is indeed the correct class
I've made sure that the managedObjectContext I'm saving is the same as the object's context
I haven't made any changes to the auto-generated setter/getter methods of my managed object classes
I've tried using the setValue:forKey: method instead of object's properties
I've used the -com.apple.CoreData.SQLDebug 1 argument to log Core Data SQL, and no SQL is logged when I update and save the object's properties
I do not really understand your statement
WCAAssessmentResult *result = [self currentResult];
Indeed, if you are accessing a to-many relationship from an object, you should get back a set, not an object. Anyway, without seeing the code it's hard to tell. The problem you are experiencing may or may not lie there.
I would rather expect in your code something like the following snippet to access objects belonging to a to-many relationship. I assume that yourObject is the object you use to access the WCAAssessmentResult objects in the to-many relationship, which I call results.
NSMutableSet *resultObjects = [yourObject mutableSetValueForKey:#"results"];
NSPredicate *predicate = ...
[resultObjects filterUsingPredicate:predicate];
for(WCAAssessmentResult *result in resultObjects){
// modify as needed the current result object
}
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"save failed");
NSLog(#"%#",[error description]);
}
Did you verify that the managedObjectContext you are using to save the object is valid (not nil) ?
Some ideas in no particular order:
I would log the class of the result object and make sure it is the class you think it is. Some confusion with super/sub classes could result in certain values not being saved.
If you made any alterations in the setter/getter methods in any class in the hierarchy, look closely at those methods especially if you used a primativeValue method. Simply leaving out willChangeValue and didChangeValue can cause changes to be invisible to the context and sometimes to the object itself.
I would log the context you are saving as well as managedObjectContext property of the result object. Confirm they are indeed the same context.
Skip using the property accessors (the dot notation) and use setValue:forKey and see if that makes any difference. If so, you have an accessor problem. Likewise, you could try setPrimativeValue:forKey to also check for an accessor problem.
If I had to bet, I would put my money on you assigning the objects returned by a fetch to the wrong class.
I would like to know how to implement a validation in Core Data. What I'd like to do is ensure that an attribute is unique within the scope of a related parent object. In other words, I'm wondering how to implement the validates_uniqueness_of :field, :scope => :parent paradigm (from rails / activerecord) in Core Data.
For example, suppose I create two models - one called Blog and one called Post. Each Post has an attribute called title. Different Blog objects can have Posts with identical titles, but how do I validate the uniqueness of a title within the scope of a Blog?
Thanks!
Walk the relationship to the parent and grab the set of posts. Then you can run a predicate against it to check for uniqueness like:
NSSet *set = [[self parent] posts];
NSSet *filtered = [set filteredSetWithPredicate:[NSPredicate preicateWithFormat:#"self != %# and title == %#", self, [self title]]];
if ([filtered count] > 0) return NO;
return YES;