Search code examples
iosdatabasecore-datarelational-databasedatabase-deadlocks

Database design for recipe app with shopping list feature (iOS CoreData)


I'm working on a recipe app that will have a shopping list feature and I'd like to know if approach is not wrong.

Recipe (one to many)
- title
- serving
- image meta
- ingredients
- steps
- note

Recipe Category (has many Recipe)
- main
- side
- ...

Shopping List (has many recipe ingredients)
- recipe[3].ingredients[2]
- recipe[5].ingredients[3]
- recipe[5].ingredients[4]
- ...

So basically Shopping List entities will have reference of ingredients from Recipe entity. I understand that it's best to make Ingredient as a separate entity but since users may enter ingredients in an inconsistent way with / without amount, I chose to make it an attribute to Recipe entity.

I believe Core Data is some kind of ORM, like Doctrine in PHP and I've seen a number of use cases where each entity doesn't necessarily have an id attribute. In a traditional relational database, the relationship is built upon id attribute of mutual entities but Core Data seems to be able to establish a relationship without the need of id.

So my approach with Shopping List entity is that, I will make a reference of ingredient like Recipe[3].ingredients[4] and I wonder if this is an acceptable practice or even just plain wrong way of thinking.

Since Shopping List entity will be a pure reference of other entities, I don't even know if I need to have an entity for that. If iPhone has some kind of local storage, can I just use that instead? It's different question but I just want to know the right approach for persisting Shopping List records.

UPDATE

Shopping List entity should also store custom items that user will add via custom field. I wonder if the following diagram establishes this relationship correctly.

enter image description here


Solution

  • Set up your ingredients as independent objects like this:

    ID_1 = {title:"Tomato", x:y, p:k etc}
    ID_2 = {title:"Bread"}
    ID_3 = {title:"Cheese"}
    ID_4 = {title:"Sugar"}
    

    Set up your recipes objects like this:

    ID_A = {ingredients:[ID_1, ID_2, ID_3], title:"Margherita", serving:2 }
    ID_B = {ingredients:[ID_2, ID_3], title:"Cheese on Toast", serving:1 }
    ID_C = {ingredients:[ID_4], title:"Caramel", serving:6 }
    

    Set up your shopping lists like this:

    ID_k = {purchase:[ID_1,ID_2,ID_3,ID_4] timestamp:[NSDate date]}
    

    For custom ingredients (entered by the user) add to your ingredients database dynamically and reference in the same way, e.g.

    ID_5 = {title:"Custom Ingredient"}
    

    You've captured the relationship between recipes and their ingredients (and any characteristics those ingredients might have, such as nutritional value) as well as the relationship between shopping lists and ingredients.

    If you need to tie the relationship between shopping list and recipe, or if you only want to buy some ingredients in a recipe, you could do something like this:

    ID_k = [{recipe:ID_A, ingredients:[ID_1,ID_2] timestamp:[NSDate date]},{}]
    

    UPDATE

    When the user starts to enter an ingredient, e.g. "To", do a string search of your ingredients database and make suggestions, such as "Tomato". Where no existing entries can be found, allow the user to add their own. This will reduce the number of redundant entries.

    For quantity, add an additional variable to the ingredients objects, e.g.

    ID_1 = {title:"Tomato Puree", measurement:"liter"}
    ID_2 = {title:"Bread", measurement:"gram"}
    ID_3 = {title:"Cheese", measurement:"gram"}
    

    in the recipes section:

    ID_A = {ingredients:[ID_1, ID_2, ID_3], quantity:[0.5, 200, 300], title:"Margherita", serving:2 }
    

    and in the shopping list objects:

    ID_k [{recipe:ID_A, ingredients:[ID_1,ID_2], quantity:[2,300,200], timestamp:[NSDate date]}, {}]
    

    When displaying the data to the user, you can check the measurement type of the ingredient first and replace the float value with a more human-readable string.

    For instance, if you're quantity is "0.5" and your measurement is "literal" (or something like that), you can display "half", or 0.25 is "quarter" etc. For other measurement types, e.g. "gram", just display the quantity and the measurement directly, e.g. 300 grams.