Search code examples
core-datansfetchedresultscontrollernsmanagedobjectnsmanagedobjectcontextnsset

How to Add target NSManagedObject to another one with an inverse many-to-many Core Data relationship without duplicating target NSManagedObject?


I had encountered an issue one of my projects, I had spent dozen of hours to find the solution, I had made some progress, but still not getting what I would like to achieve ideally. I am still finding solutions myself now, while I would really appreciate anyone could share any insight for constructive solutions.

Issue:

  • How to add a target NSManagedObject from a master list as one object of the references in another NSManagedObject with an inverse many-to-many relationship between each NSManagedObject without creating duplicate target NSManagedObject in the master list.

Note:

  • The following example used an analogy to the full data model graphs to my real project. The analogy is what I can best describe the issue I have, while the objects in the analogy do not fully share the same name of the objects in the real project.

What I have now:

  • A master list of ingredient objects, which are unique among each
    other.

  • A list of recipe objects, each of which would like to have different ingredient objects to define the recipe object.

What do I want to achieve:

  • Ingredient objects can be inserted as multiple times into a single recipe object with each insert as unique count instead of making same ingredient being considered as one single count.

  • I did not want to duplicate each ingredient object inside of the master list to be able to add multiple ingredients objects to each recipe object or cross multiple recipe objects.

What I had tried:

  • Use Core Data to manage the ingredient and recipe as 2 NSManagedObjects.

  • Had created a relationship attributes called “allHostRecipes” on the ingredient managed object, and set it as “to-Many“ relationship to the recipe managed object

  • Had created a relationship attributes called “allUsedIngredients” on the recipe managed object, and set it as “to-Many“ relationship to the ingredient managed object.

  • These two relationships are set as “inverse”.
  • I had a Recipe Description View with a Table View that lists all the ingredients that are and will be included inside of the recipe.
  • I created another Ingredients Selection Table View that can be triggered in the recipe description view to pick each ingredient,which is going to be added into the recipe.
  • Each time when an ingredient is picked in the Ingredients Selection Table View, I call objectAtIndexPath(_:) on the NSFetchedResultsController that is for ingredients Table View from the ingredients’ master list to find the selected ingredient objects in its ManagedObjectContext.
  • Then I passed the selected ingredient managed object (SelectedIngredientManagedObject) back to Recipe Description View and called mutableSetValueForKey("allUsedIngredients").addObject(SelectedIngredientManagedObject) on the NSFetchedResultsController that is for fetching ingredients that is contained inside of a recipe object.

  • The “NSFetchedResultsController that is for ingredients Table View from the ingredients’ master list” and “NSFetchedResultsController that is for fetching ingredients that are contained inside of a recipe object” are separate instance variables in “Table Views of Recipe Description View” and “Ingredients Selection Table View”. But they referenced the same ManagedObjectContext.

What I had got now:

  • The selected ingredient managed object can be added to the recipe.
  • But, if I selected the same ingredient multiple times, it only get count once in the Table Views of Recipe Description View instead of showing multiple counts by each inserting, which is NOT what I want to achieve as described above.

My Question:

  • What should I do or adjust to achieve the functionalities that I had to describe above?

What I think the directions to solve the issue:

  • What other things should I do when defining the “Many-to-Many” relationship in Core Data model?
  • Does the fact that the “to-Many“ reference is using an NSSet cause the count issue?
  • Is it necessary to create multiple ManagedObjectContext to achieve the desired functionalities?
  • Should I clone the selected ingredient managed object as a new ingredient managed object? Which I had tried, and it will add duplicated ingredient to the ingredients’ master list. This is also NOT what I want. If I need to clone it, how can I make it right?

I really appreciate your time to view it, I am looking forward to having your insights. Thank you very much.

Regards,

Knight


Solution

  • You need to remodel the data slightly: Remove the many-many relationship from Recipe to Ingredient, and replace it with an intermediate entity (finding a good name is difficult, let's say RecipeIngredientDetails).

    Create a one-many relationship from Recipe to RecipeIngredientDetails, say allUsedIngredientDetails, with inverse (to-one) recipe.

    Likewise create a one-many relationship from Ingredient to RecipeIngredientDetails, say allHostRecipeDetails, with inverse (to-one) ingredient.

    This addresses the problem with a direct many-many relationship, where each Recipe can be related to each Ingredient only once. (You are correct, this is in part a consequence of the relationships being modelled as Sets, which cannot have duplicate members). You have two options: you could just add multiple RecipeIngredientDetails objects, each related to the same Recipe and Ingredient pair. Each such object might represent a standard base quantity of the ingredient. Note that you could not have just one object for each Recipe/Ingredient pair, and try to add that object to the same Recipe multiple times: a given Recipe and a given RecipeIngredientDetails object can be related at most once.

    But it might be better to add an attribute to the RecipeIngredientDetails, say quantity. You then only need a single such object for each Recipe/Ingredient pair, and can update the quantity attribute to reflect the amount of the ingredient that is appropriate for that recipe.

    This is the approach mentioned in the CoreData Programming Guide section on Modeling a Relationship Based on Its Semantics:

    For this sort of relationship, use an intermediate (join) entity. An advantage of the intermediate entity is that you can also use it to add more information to the relationship.

    It is equivalent to adding a join table with foreign keys and additional columns to a SQL database. I'm not aware of any simpler way of achieving your objectives in CoreData - there is no way to directly add attributes to relationships.

    Regarding the ordering issue that you mention in comments, you had added "a Double type attribute in the Entity to keep track of the order". If you have only two entities, and a many-many relationship, and you add the order attribute to the Ingredient then (for example) if "Flour" is the first ingredient for "Bread", it would have to be the first item for every other Recipe it is used in. In the approach I describe, you would add the attribute to the intermediate entity, RecipeIngredientDetails: the index (just as for the quantity) depends on both the recipe and the ingredient.

    For indexes there is, however, another option I should mention: you could (in the data model editor) define the relationship from Recipe to RecipeIngredientDetails as ordered. The resulting property will be an ordered set (so you can insert, remove or move items to achieve the correct order).