Search code examples
objective-ccore-dataosx-yosemitemagicalrecord

disallow change to core data relationship, but eventually allow for delete of the child


I have a core data model with two entities, 'Parent' and 'Child'. Parent has a to-many relationship to child, and the child has a to-one relationship to parent. I want to prevent changes to the parent relationship in the child once its parent has been set. The delete of the child, however, should be allowed.

The child's setParent looks like this:

- (void)setParent:(Parent *)parent {
   if (self.parent) return;
   [self willChangeValueForKey:@"parent"];
   [self setPrimitiveValue:parent forKey:@"parent"];
   [self didChangeValueForKey:@"parent"];
  }

Now, this prevents the change of the parent once it's set, but at the same time, it will prevent the delete of the child, because setParent gets revisited twice during delete to set the parent to nil.

The first time setParent is called during delete, self.isDeleted is true. So I can react to this situation. But setParent is called yet again during delete, and this time self.isDeleted is false, and I have no idea how to know if someone tries to edit the parent relationship or if a delete is happening.

I am working with MagicalRecord 2.30 and the delete call looks like this:

[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext)      {

    [sut MR_inContext:localContext];
    [sut MR_deleteEntityInContext:localContext];
}];

I've looked all over to find some info about those setter calls during delete, but no luck. So any help would be appreciated.

EDIT I put some NSLogs into my code to document was is happening. The delete actually works with this code, but I have no clue what all these calls are about. Here is the relevant code:

- (BOOL) MR_deleteEntityInContext:(NSManagedObjectContext *)context {
    NSLog(@"|");
    NSLog(@"|");
    NSLog(@"----------------------------------");
    NSLog(@"| ***** starting delete... ***** |");
    NSLog(@"----------------------------------");
    NSLog(@"|");
    NSLog(@"|");
    return [super MR_deleteEntityInContext:context];
}

- (void)setParent:(Parent *)parent {
    NSLog(@"|");
    NSLog(@"--------------------------------------------------------------------------");
    NSLog(@"setParent has been called with parameter <%p>", parent);
    NSLog(@"                          self.parent is: %p", self.parent);
    NSLog(@"self.isDeleted is: %hhd", self.isDeleted);
    NSLog(@"         self.moc: %@", self.managedObjectContext);
    NSLog(@"--------------------------------------------------------------------------");
    NSLog(@"|");
    if (!self.isDeleted && self.parent) return;
    [self willChangeValueForKey:@"parent"];
    [self setPrimitiveValue:parent forKey:@"parent"];
    [self didChangeValueForKey:@"parent"];
}

It produces the following output:

 ----------------------------------
 | ***** starting delete... ***** |
 ----------------------------------
 |
 |
 |
 --------------------------------------------------------------------------
 setParent has been called with parameter <0x0>
                           self.parent is: 0x6080000a9cc0
 self.isDeleted is: 1
          self.moc: <NSManagedObjectContext: 0x6000001c05a0>
 --------------------------------------------------------------------------
 |
 |
 --------------------------------------------------------------------------
 setParent has been called with parameter <0x0>
                           self.parent is: 0x6080000aa080
 self.isDeleted is: 0
          self.moc: <NSManagedObjectContext: 0x6080001c0a50>
 --------------------------------------------------------------------------
 |
 |
 --------------------------------------------------------------------------
 setParent has been called with parameter <0x0>
                           self.parent is: 0x6080000aa080
 self.isDeleted is: 1
          self.moc: <NSManagedObjectContext: 0x6080001c0a50>
 --------------------------------------------------------------------------
 |

Solution

  • Here is what I found. I've changed the above code a little to also log the inverse relationship.

    - (void)setParent:(Parent *)parent {
        NSLog(@" ");
        NSLog(@"--------------------------------------------------------------------------");
        NSLog(@"| setParent has been called with parameter <%p>", parent);
        NSLog(@"|                                 self is: %p", self);
        NSLog(@"|                          self.parent is: %p", self.parent);
        NSLog(@"|                parent.children contains: %p", [Parent MR_findFirstInContext:self.managedObjectContext].children.allObjects.firstObject);
        NSLog(@"| self.isDeleted is: %hhd", self.isDeleted);
        NSLog(@" ");
        if (!self.isDeleted && self.parent) return;
        if (self.isDeleted) [self setPrimitiveValue:nil forKey:@"parent"];
        [self willChangeValueForKey:@"parent"];
        [self setPrimitiveValue:parent forKey:@"parent"];
        [self didChangeValueForKey:@"parent"];
        NSLog(@"|- after setting the parent ...");
        NSLog(@"|-                                self is: %p", self);
        NSLog(@"|-                         self.parent is: %p", self.parent);
        NSLog(@"|-               parent.children contains: %p", [Parent MR_findFirstInContext:self.managedObjectContext].children.allObjects.firstObject);
        NSLog(@"--------------------------------------------------------------------------");
    }
    

    When I delete the child the following is logged:

    ----------------------------------
    | ***** starting delete... ***** |
    ----------------------------------
    |
    |
    
    --------------------------------------------------------------------------
    | setParent has been called with parameter <0x0>
    |                                 self is: 0x6000000a79e0
    |                          self.parent is: 0x6000000a7a40
    |                parent.children contains: 0x6000000a79e0
    | self.isDeleted is: 1
    
    |- after setting the parent ...
    |-                                self is: 0x6000000a79e0
    |-                         self.parent is: 0x0
    |-               parent.children contains: 0x6000000a79e0
    --------------------------------------------------------------------------
    
    --------------------------------------------------------------------------
    | setParent has been called with parameter <0x0>
    |                                 self is: 0x6080000a8100
    |                          self.parent is: 0x6080000a8160
    |                parent.children contains: 0x0
    | self.isDeleted is: 0
    
    
    --------------------------------------------------------------------------
    | setParent has been called with parameter <0x0>
    |                                 self is: 0x6080000a8100
    |                          self.parent is: 0x6080000a8160
    |                parent.children contains: 0x0
    | self.isDeleted is: 1
    
    |- after setting the parent ...
    |-                                self is: 0x6080000a8100
    |-                         self.parent is: 0x0
    |-               parent.children contains: 0x0
    --------------------------------------------------------------------------
    

    So, the first and the third time the setter is called during delete, isDeleted is true. But on the second call of the setter isDeleted is false. If I test the inverse relationship at this point, I see that it has been deleted already. That test is good enough for me. It allows the delete cycle to pass through, and at the same time allows running the setter if the relationship has not been set at all.