3

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?

Rog
  • 18,602
  • 6
  • 76
  • 97

2 Answers2

2

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.

Community
  • 1
  • 1
Wienke
  • 3,723
  • 27
  • 40
  • +1 Good catch on the to-one `Tag-->MeettingNote`. I missed that when I read the parent. – TechZen Jun 27 '11 at 12:45
  • +1 thanks for your answer, you are right about the MeetingNote x Tag relationship. The idea is that a meeting note can have many tags and a tag can have many meeting notes associated with it, meaning that at any point in time I could query the tag `task` and get all the meeting notes intances that have been assigned the `task` tag. – Rog Jun 29 '11 at 13:53
2

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:

  1. A MeetingNote object cannot have a related Task object without also having a Tag object with name=="task".
  2. If the MeetingNote object does have a Tag object with name=="task" it must have at least one related Task object.
  3. 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.

TechZen
  • 64,370
  • 15
  • 118
  • 145
  • Really nice having that example. May I ask for an additional explanation of the need for keyValue observing here, the bracketing with willChangeValueForKey and didChangeValueForKey? Why wouldn’t `self.meetingNotes = changedObjects;` do? Thanks. – Wienke Jun 28 '11 at 02:27
  • Not sure, to be honest, as it has been a while since I tried it. All the Apple code I have seen gets the existing mutable set object and then changes that. I don't think you get all the KVO notifications you need if you just swap out the entire set. If nothing else, the objects inside the referenced set won't get any KVO notifications because nothing has changed for them. I don't think there is even a `replaceAnEntireSet` notification which you would need to make sure all the KVO for all the involved objects worked. – TechZen Jun 28 '11 at 20:01
  • Thanks for the info. Not knowing better, the simple set assignment is what I actually used for my current project, and so far my tables and checkboxes are updating correctly and the data persists -- but now I'll know what to suspect first if something goes wrong. Your explanations are appreciated. – Wienke Jun 29 '11 at 00:33
  • Wow this is way more than what I expected, and it makes complete sense to create a TagTask entity as a subclass of Tag. I am still wrestling with the entire database modelling at the moment but your insights will be really useful when I finally get to start coding things away. Out of curiosity, would you normally have a final modelling of your objects and relationships before you stat coding, or is this something that you choose to do concurrently with the implementation of your code? Thanks again, your help is extremely appreciated. – Rog Jun 29 '11 at 13:49
  • @Rog -- I think it good practice to get your data model nailed down before you code anything. The data model is the actual logical guts of your app and everything else is just one form of interface or another. Ideally, you data model should be usable with any kind of interface e.g GUI, command line or web page. In my experience, once the data model layer is completed you've got 75% of the app and the rest just falls naturally into place. – TechZen Jun 29 '11 at 18:15