Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data Model Design

Let's assume I have an app about cooking recipes with two fundamental features:

  1. The first one involves the CURRENT recipe that I'm preparing
  2. The second one stores the recipes that I've decided to save

STANDARD SCENARIO

My current recipe is "Cheese Cake" and in RecipeDetailViewController I can see the current ingredients I've added for this recipe:

  • Sugar
  • Milk
  • Butter
  • etc.

Well, let's say that I'm satisfied from the final result and I decide to save (to log) the recipe I've just prepared.

* click save *

The recipe is now saved (is now logged) and in RecipesHistoryViewController I can see something like this:

  • Nov 15, 2013 - Cheese Cake
  • Nov 11, 2013 - Brownie
  • etc.

Now if I want I can edit the recipe in the history and change Milk to Soy Milk, for example.

The issue it's that editing the recipe in the history SHOULDN'T edit the recipe (and its ingredients) in my current recipe and vice versa. If I edit the current recipe and replace Butter with Peanut Butter it must not edit anyone of the recipe stored in history. Hope I explained myself.

CONSEQUENCES

What this scenario implies? Implies that currently, for satisfing the function of this features, I'm duplicating the recipe and every sub-relationship (ingredients) everytime the user click on "Save Recipe" button. Well it works but I feel it can be something else more clean. With this implemention it turns out that I have TONS of different duplicates Core Data object (sqlite rows) like these:

  • Object #1, name: Butter, recipe: 1
  • Object #2, name: Butter, recipe: 4
  • Object #3, name: Butter, recipe: 3

etc.

Ideas? How can I optimize this model structure?

EDIT 1

I've already thought of creating any RecipeHistory object with an attribute NSString where I could store a json dictionary but I don't know if it's better or not.

EDIT 2

Currently a RecipeHistory object contains this:

+-- RecipeHistory --+
|                   |
| attributes:       |
| - date            |
+-------------------+
| relationships:    |
| - recipes         |
+-------------------+

+----- Recipe ------+
| relationships:    |
| - recipeInfo      |
| - recipeshistory  |
| - ingredients     |
+-------------------+

+-- RecipeInfo  ----+
|                   |
| attributes:       |
| - name            |
+-------------------+

+--- Ingredient ----+
|                   |
| attributes:       |
| - name            |
+-------------------+
| relationships:    |
| - recipe          |
+-------------------+

paulrehkugler is true when he says that duplicating every Recipe object (and its relationships RecipeInfo and Ingredients) when I create a RecipeHistory is going to fill the database with a tons of data but I don't find another solution that allows me flexibility for the future. Maybe in the future I would to create stats about recipes and history and having Core Data objects could prove to be useful. What do you think? I think this is a common scenario in many apps that store history and allow to edit history item.


BIG UPDATE

I have read the answers from some users and I want to explain better the situation. The example I stated above is just an example, I mean that my app doesn't involve cook/recipe argument but I have used recipes because I think it's pretty okay for my real scenario.

Said this I want to explain that the app NEEDS two sections: - First: where I can see the CURRENT recipe with related ingredients - Second: where I can see the recipe I decided to save by tapping a button 'Save Recipe' in the first section

The current recipe found in the first section and a X recipe found in the 'history' section doesn't have NOTHING in common. However the user can edit whatever recipes saved in 'history' section (he can edit name, ingredients, whatever he wants, he can completely edit all things about a recipe found in history section).

This is the reason why I came up duplicating all NSManagedObjects. However, in this way, the database will grow as mad because everytime the user saves the current recipe the object representing the recipe (Recipe) is duplicated and also the relationships the recipes had (ingredients). So there will be TONS of ingredients named 'Butter' for example. You can say me: why the hell you need to have TONS of 'Butter' objects? Well, I need it because ingredients has for example the 'quantity' attribute, so every recipe have ingredients with different quantities.

Anyhow I don't like this approach, even it seems to be the only one. Ask me whatever you want and I'll try to explain every detail.

PS: Sorry for my basic English.

EDIT

enter image description here

like image 753
Fred Collins Avatar asked Nov 15 '13 02:11

Fred Collins


3 Answers

Since you must deal with history, and because the events are generated manually by end users, consider changing the approach: rather than storing the current view of the model entities (i.e. recipes, ingredients, and the connections among them) store the individual events initiated by the user. This is called Event Sourcing.

The idea is to record what user does, rather than recording the new state after the user's action. When you need to get the current state, "replay" the events, applying the changes to in-memory structures. In addition to letting you implement the immediate requirements, this would let you restore the state as of a specific date by "replaying" the events up to a certain date. This helps with audits.

You can do it by defining events like this:

  • CreateIngredient - Adds new ingredient, and gives it a unique ID.
  • UpdateIngredient - Changes an attribute of an existing ingredient.
  • DeleteIngredient - Deletes an ingredient from the current state. Deleting an ingredient deletes it from all recipes and recipe histories.
  • CreateRecipe - Adds a new recipe, and gives it a unique ID.
  • UpdateRecipeAttribute - Changes an attribute of an existing recipe.
  • AddIngredientToRecipe - Adds an ingredient to an existing recipe.
  • DeleteIngredientFromRecipe - Deletes an ingredient from an existing recipe.
  • DeleteRecipe - Deletes a recipe.
  • CreateRecipeHistory - Creates a new recipe history from a specific recipe, and gives the history a new ID.
  • UpdateRecipeHistoryAttribute - Updates an attribute of a specific recipe history.
  • AddIngredientToRecipeHistory - Adds an ingredient to a recipe history.
  • DeleteIngredientFromRecipeHistory - Deletes an ingredient from a recipe history.

You can store the individual events in a single table using Core Data APIs. Add a class that processes events in order, and creates the current state of the model. The events will come from two places - the event store backed by Core Data, and from the user interface. This would let you keep a single event processor, and a single model with the details of the current state of recipes, ingredients, and recipe histories.

Replaying the events should happen only when the user consults the history, right?

No, that is not what happens: you read the whole history on start-up into the current "view", and then you send the new events both to the view and to the DB for persistence.

When users need to consult the history (specifically, when they need to find out how the model looked as of a specific date in the past) you need to replay the events partially, up until the date of interest.

Since the events are generated by hand, there wouldn't be too many of them: I would estimate the count in the thousands at the most - that's for a list of 100 recipes with 10 ingredients each. Processing an event on a modern hardware should be in microseconds, so reading and replaying the entire event log should be in the milliseconds.

Furthermore, do you know any link that shows an example of how to use Event Sourcing in a Core Data application? [...] For example, should I need to get rid of RecipeHistory NSManagedObject?

I do not know of a good reference implementation for event sourcing on iOS. That wouldn't be too different from implementing it on other systems. You would need to get rid of all tables that you currently have, replacing it with a single table that looks like this:

Event log entry

The attributes would be as follows:

  • EventId - Unique ID of this event. This is assigned automatically on insertion, and never changes.
  • EntityId - Unique ID of the entity created or modified by this event. This ID is assigned automatically by a Create... processor, and never changes.
  • EventType - A short string representing the name of this event type.
  • EventTime - The time the event has happened.
  • EventData - A serialized representation of the event - this can be binary or textual.

The last item can be replaced for a "denormalized" group of columns representing a superset of attributes used by the 12 event types above. This is entirely up to you - this table is merely one possible way of storing your events. It does not have to be Core Data - in fact, it does not even need to be in a database (although it makes things a little easier).

like image 137
Sergey Kalinichenko Avatar answered Oct 20 '22 20:10

Sergey Kalinichenko


I think when a row in RecipesHistoryViewController is selected to modification, we can optimize the Save process with two options:

  • Let the user chooses if a new row must be saved or an update may happen.
    Having a Save New button to create a new row in Recipe and an Update button to update the current selected row.
  • To trace the changes have been made to a recipe (when update happens), I will try to log only changes of the recipe.
    Using EAV pattern will be an option.

    enter image description here]![enter image description here]![enter image description here As a hint:
    Comma separated values of ingredient name could be used as old and new values, when inserting a row in RecipeHistory table, the sample may helps.

About the BIG UPDATE:
Assuming that the real application have a database for persistent operation, some suggestions may be helpful.

The current recipe found in the first section and a X recipe found in the 'history' section doesn't have NOTHING in common

Leads the natural way of having no relation between Current and In-History recipe, so trying to create a relation will be vain. With no relation the design will not be in normal form, redundancy will be inevitable.
Flowing the approach there will be many records, in the case

  • We can limit any user's saved recipes in a predefined number.

  • Another solution to optimize performance of recipe table would be range partitioning the table based on creation date field (let a data base administrator be involved).

  • Another suggestion is to have a separate table for ingredient concept.
    Having ingredient, recipe, recipe-ingredient tables will reduce redundancy.
    enter image description here

Using NoSql
If relations are not trivial part of the applications logic, I mean if your are not going to be ended in complex queries like "Which ingredients have been used more than X times in recipes that have less than total Y ingredients and Milk is not one of them" or analytical procedures then,have a look at NoSql databases and comparison of them.

They offer being non-relational, distributed, open-source, schema-free, easy replication support, simple API, huge amount of data and horizontally scalable.

For a basic example of a document based database:
Having couchdb installed on my local machine(port number 5984) creating recipe database(table) on couchdb will be done by sending an standard HTTP request (using curl) like:

curl -X PUT http://127.0.0.1:5984/recipe 

Dropping recipe table:

curl -X DELETE http://127.0.0.1:5984/recipe 

Adding a recipe:

curl -X PUT http://127.0.0.1:5984/recipe/myFirstRecipe -d 
'{"name":"Cheese Cake","description":"i am using couchDB for my recipes",
"ingredients": [ 
"Milk", 
"Sugar" 
],}'

Getting myFirstRecipe record(document)

curl -X GET http://127.0.0.1:5984/recipe/myFirstRecipe 

No need of classical server side process like object relation mapping, data base driver, etc
BTW using Nosql will have short comings you need to consider, like here and here.

like image 4
Mohsen Heydari Avatar answered Oct 20 '22 19:10

Mohsen Heydari


As I see it, your problem is more conceptual than model structure related.
My idea for your model is:

+*******+
Recipe
-----------------

-----------------
properties:
-----------------
- isDraft - BOOL
- name - NSString
- creationDate - NSDate
-----------------

-----------------
relationships:
-----------------
- ingredients - to-many with Ingredient
-----------------
+*******+

+*******+
Ingredient
-----------------

-----------------
properties:
-----------------
- name - NSString
-----------------

-----------------
relationships:
-----------------
- recipes - to-many with Recipe
-----------------

+*******+

Now, Lets call your "current" recipe a draft (a user may have many drafts).
As you can see, you can now display your recipes with a single fetched results controller (FRC)
The fetch request will look like this:

NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:@"Recipe"];
[r setFetchBatchSize:25];

NSSortDescriptor* sortCreationDate = [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO];
[r setSortDescriptors:@[sortCreationDate]];

you can section your data on the isDraft property:

NSFetchedResultsController* frc = [[NSFetchedResultsController alloc] initWithFetchRequest:r
                                                                      managedObjectContext:context
                                                                        sectionNameKeyPath:@"isDraft"
                                                                                 cacheName:nil];

Remember to give appropriate titles to your sections as to not confuse the user.

Now, all you have left is add some specific functionality like:

  • create new recipe
    • save
    • save draft
  • edit recipe (draft or not)
    • if draft offer to save as complete recipe
    • else, save the actual recipe
    • if you like, you might add a "save as" option
  • create copy (the user is aware that he might introduce redundant data if he saves the same recipe more than once)

In any case the user experience should be consistent.
Meaning:
While the user is editing/adding an object, this object should not change "under his feet".
If a user is adding a new recipe, he then might wish to save it as draft, or as a complete recipe.
When he save, in either case, he might still wish to continue editing it. and so, no new object need be created.

If you like to add versioning for your recipes, you will need to add an entity like RecipeHistory related to a single recipe. this entity will record changes on each committed change in a complete recipe object (use changedValues of NSManagedObject or check against the existing/committed values).
You may serialise and store the data as you see fit.

So you can see, its more of a conceptual issue (how you access your data) than it is a modelling issue.

like image 2
Dan Shelly Avatar answered Oct 20 '22 20:10

Dan Shelly