How to force UNIDIRECTIONAL to-many relationship to persist

前端 未结 3 1924
無奈伤痛
無奈伤痛 2021-01-06 17:25

There is a problem with core data when a to-many relationship has no inverse. Changes made to the related property do not persist. This is a problem many of us have faced, a

相关标签:
3条回答
  • 2021-01-06 17:50


    This is a great question.

    ButFirst thing first:
    It clearly state in the documentation:

    "Important: You must define many-to-many relationships in both directions—that is, you must specify two relationships, each being the inverse of the other. You can’t just define a to-many relationship in one direction and try to use it as a many-to-many. If you do, you will end up with referential integrity problems."

    Never the less, Lets describe the issue (resulting database)
    When defining a to-many relationship, the resulting database does not add an additional table to map the relationship.
    It only sets a property on the entity at one end of the to-many relationship equal to the last item that referenced it.

    Example:

    Model:
    Entity: Department
    Relationships: NONE
    Properties: name (string)

    Entity: Employee
    Relationships: departments (to-many,no-action)
    Properties: name

    Resulting Database:
    ZDEPARTMENT:
    Z_PK
    Z_ENT
    Z_OPT
    Z2DEPARTMENTS (int)
    ZNAME

    ZEMPLOYEE:
    Z_PK
    Z_ENT
    Z_OPT
    ZNAME

    This structure will obviously result in data inconsistency.

    The solution will be to hold an entity: DepartmentEmployee modeling the to-many relationship in both directions but one of them would be unidirectional (Department -> DepartmentEmployee):

    DepartmentEmployee:
    Relationships: department (to-one,no-action), employee (to-one,nullify)

    and you will have to maintain the table upon deletion of a department object.

    Hope this made some sense :)

    0 讨论(0)
  • 2021-01-06 17:56

    First a reply for your comment:

    IMO, and for my use case, the join entity does not even need to have the relationship to Department. This to-one is useless and may be replaced by a property of the join entity keeping related Department information, like its objectID or other indexed property to reach it.

    This is exactly what the department property is doing in the joined relationship.
    If you would look at the generated SQLite structure, you will see and additional mapping table between the Employee entity and the Department entity, holding only their int64 ids.

    Now, the given example was:

    Example

    In employees/department typical paradigm, if you have a huge number of employees able to belong to several departments, you need a to-many relationship from employee to department. You do not want the inverse because each time an employee is linked to a department, a (very) large NSSet must be loaded, updated and saved. Moreover if the department entity is never deleted, graph integrity is easy to maintain. A simple ONE-to-many relationship with no inverse could be easily implemented.
    You can look at it as just another property on the object in the 'many' side of the relationship.

    This example request a ONE-to-many relationship of the kind:
    Employee-->>Department (an Employee may belong to many departments)
    The inverse is:
    Department-->Employee
    Since we must not implement a many-to-many relationships without an inverse, we must implement the to-ONE side of the relationship, just to make sure we comply with the implementation of the framework.

    Re-iterating:
    By the documentation we know that no many-to-many relationship will NOT persist without an inverse relationship being defined.
    ==>
    Since we like to model the relationship without an inverse we will model it only as the to-ONE part of the coupling (modelling it as a to-many will violate the persistency promised by the framework)
    Think of it as useful for defining files in a folder (a file may not belong to more than one folder), or parent child relationship.
    ==>
    We must define the relationship as:
    Department-->Employee (Which does not make much sense since a department that can hold only one employee is not really a department is it)

    To look at it from another angel (negative proof):
    Suppose we would like to go against the framework and define a MANY-to-many relationship with no inverse.
    ==>
    That would mean that we will only implement it in one direction leaving a ... to-many relationship or ... MANY-to relationship
    ==>
    this is the same thing isn't it (a to-many relationship from and entity1 to entity2)
    ==>
    NOW, if we have a ONE-to-many relationship and we choose to not implement the inverse of it, we can choose to implement the to-many part? NO WE CANNOT, this will look as only half of a MANY-to-many relationship ==>
    We MUST implement the ONE-to part of it.

    For making some more sense, I will show the more logical:
    Department-->>Employee So our implementation for this ONE-to-many relationship would be:
    Department<--Employee

    This will result in the following SQLite DB structure:
    ZDEPARTMENT:
    Z_PK
    Z_ENT
    Z_OPT
    ZNAME

    ZEMPLOYEE:
    Z_PK
    Z_ENT
    Z_OPT
    ZDEPARTMENT (int)
    ZNAME

    We could now define a fetched property on Department to fetch all the employees belonging to it:
    employees predicate: department == $FETCH_SOURCE

    You can enforce this relationship in the prepareForDeletion method of Department (not tested):
    (You will first set the userInfo dictionary on Department to hold the type of enforcement)
    (I left the implementation of the 'Deny' rule to the reader :D )

    - (void) prepareForDeletion
    {
        [super prepareForDeletion];
        NSEntityDescription* entity = [self entity];
        NSDictionary* dict = [entity userInfo] ;
        if ([dict count]) {
            [dict enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL *stop) {
                NSArray* arr = [self valueForKey:key];
                if( [value isEqualToString:@"cascade"]) {
                    for (NSManagedObject* obj in arr) {
                        [[self managedObjectContext] deleteObject:obj];
                    }
                } else if ( [value isEqualToString:@"nullify"] ) {
                    NSArray* arr = [self valueForKey:key];
                    for (NSManagedObject* obj in arr) {
                        [obj setValue:nil forKey:@"department"];
                    }
    
                }
            }];
        }
    }
    

    As I see it, this is all you can do with regard to inverse relationships. If you still believe you need a many-to-many relationship, please refer to my other answer.

    Regards,
    Dan.

    0 讨论(0)
  • 2021-01-06 18:05

    Have you considered doing away with the relationship entirely and programmatically managing the foreign key on employee?

    If you have a UI which sets the property from a list of existing Departments (a pick list, etc.) you can simply take the primary key from that list and assign it as the departmentID property on your Employee.

    You should then be able to implement a validateDepartmentID:error method on your Employee object which checks that the given departmentID is valid (i.e. is in a fetched list of departments) and/or is not null so that you maintain referential integrity between the Employee and Department.

    When fetching the list of Employees in a Department, you can either use fetched properties or add an instance method to the Department which returns an instance of NSFetchedResultsController containing the Department's employee list.

    The only other thing you'd need to do is inject some deletion logic in your Department class (likely on -prepareForDeletion) to update the departmentID on any affected child records. That one depends on your business logic.

    The Apple docs on property validation cover -prepareForDeletion and -validateValue:forKey:error if you're not familiar with them.

    0 讨论(0)
提交回复
热议问题