5

假设我有一个关于烹饪食谱的应用程序,它具有两个基本功能:

  1. 第一个涉及我正在准备的当前食谱
  2. 第二个存储我决定保存的食谱

标准场景

我目前的食谱是“芝士蛋糕”,RecipeDetailViewController我可以看到我为这个食谱添加的当前成分:

  • 牛奶
  • 黄油
  • 等等

好吧,假设我对最终结果感到满意,我决定保存(记录)我刚刚准备的食谱。

*点击保存*

配方现在已保存(现已记录),RecipesHistoryViewController我可以看到如下内容:

  • 2013 年 11 月 15 日 - 芝士蛋糕
  • 2013 年 11 月 11 日 - 布朗尼
  • 等等

现在,如果我愿意,我可以编辑历史记录中的食谱并将牛奶更改为豆浆,例如。

问题是在历史记录中编辑食谱不应该在我当前的食谱中编辑食谱(及其成分),反之亦然。如果我编辑当前配方并用花生酱替换黄油,则不得编辑历史中存储的任何配方。希望我自己解释。

结果

这个场景意味着什么?暗示目前,为了满足此功能的功能,每次用户单击“保存配方”按钮时,我都会复制配方和每个子关系(成分)。好吧,它有效,但我觉得它可以是其他更干净的东西。通过这种实现,我发现我有大量不同的重复核心数据对象(sqlite 行),如下所示:

  • 对象 #1,名称:黄油,配方:1
  • 对象 #2,名称:黄油,配方:4
  • 对象 #3,名称:黄油,配方:3

等等

想法?如何优化此模型结构?

编辑 1

我已经考虑过创建任何带有属性的RecipeHistory对象NSString,我可以在其中存储 json 字典,但我不知道它是否更好。

编辑 2

目前一个RecipeHistory对象包含这个:

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

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

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

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

paulrehkugler 说,Recipe当我创建一个对象时复制每个对象(及其关系 RecipeInfo 和成分)RecipeHistory会用大量数据填充数据库,但我没有找到另一个解决方案来让我在未来具有灵活性。也许将来我会创建关于食谱和历史的统计数据,并且拥有 Core Data 对象可能会被证明是有用的。你怎么看?我认为这是存储历史记录并允许编辑历史记录项的许多应用程序中的常见场景。


大更新

我已经阅读了一些用户的答案,我想更好地解释这种情况。我上面提到的示例只是一个示例,我的意思是我的应用程序不涉及烹饪/食谱参数,但我使用了食谱,因为我认为这对于我的真实场景来说还不错。

说到这里,我想解释一下该应用程序需要两个部分:-第一:我可以在其中看到包含相关成分的当前食谱-第二:我可以在其中看到我决定通过点击第一个按钮“保存食谱”来保存的食谱部分

在第一部分找到的当前配方和在“历史”部分找到的 X 配方没有任何共同之处。然而,用户可以编辑保存在“历史”部分中的任何食谱(他可以编辑名称、成分,无论他想要什么,他都可以完全编辑关于历史部分中找到的食谱的所有内容)。

这就是我想复制所有NSManagedObjects 的原因。但是,通过这种方式,数据库将变得疯狂,因为每次用户保存当前配方时,表示配方 ( Recipe) 的对象以及配方之间的关系(成分)都会被复制。例如,会有大量名为“黄油”的成分。你可以说我:为什么你需要大量的“黄油”物体?好吧,我需要它,因为配料具有例如“数量”属性,所以每个食谱都有不同数量的配料。

无论如何,我不喜欢这种方法,即使它似乎是唯一的一种。你想问我什么,我会尽力解释每一个细节。

PS:对不起我的基本英语。

编辑

在此处输入图像描述

4

10 回答 10

7

由于您必须处理历史,并且由于事件是由最终用户手动生成的,因此请考虑更改方法:而不是存储模型实体的当前视图(即配方、成分和它们之间的连接)存储启动的单个事件由用户。这称为事件溯源

这个想法是记录用户做了什么,而不是记录用户操作后的新状态。当您需要获取当前状态时,“重播”事件,将更改应用于内存结构。除了让您实现即时要求之外,这还可以让您通过“重播”到某个日期的事件来恢复特定日期的状态。这有助于审计。

您可以通过定义这样的事件来做到这一点:

  • CreateIngredient- 添加新成分,并赋予其唯一 ID。
  • UpdateIngredient- 更改现有成分的属性。
  • DeleteIngredient- 从当前状态中删除一种成分。删除一种成分会将其从所有食谱和食谱历史记录中删除。
  • CreateRecipe- 添加一个新的配方,并给它一个唯一的 ID。
  • UpdateRecipeAttribute- 更改现有配方的属性。
  • AddIngredientToRecipe- 在现有配方中添加成分。
  • DeleteIngredientFromRecipe- 从现有配方中删除一种成分。
  • DeleteRecipe- 删除一个配方。
  • CreateRecipeHistory- 从特定配方创建新的配方历史,并为历史赋予新的 ID。
  • UpdateRecipeHistoryAttribute- 更新特定配方历史的属性。
  • AddIngredientToRecipeHistory- 将成分添加到食谱历史记录中。
  • DeleteIngredientFromRecipeHistory- 从配方历史中删除一种成分。

您可以使用 Core Data API 将单个事件存储在单个表中。添加一个按顺序处理事件的类,并创建模型的当前状态。事件将来自两个地方 - Core Data 支持的事件存储和用户界面。这将让您保留一个事件处理器和一个模型,其中包含配方、成分和配方历史的当前状态的详细信息。

只有在用户查阅历史记录时才会重放事件,对吧?

不,这不是发生的事情:您将启动时的整个历史记录读入当前的“视图”,然后将新事件发送到视图和数据库以进行持久性。

当用户需要查阅历史记录时(特别是,当他们需要了解模型在过去某个特定日期的样子时),您需要部分重播事件,直到感兴趣的日期。

由于事件是手动生成的,因此不会太多:我最多估计数以千计 - 这是一个包含 100 个食谱的列表,每个食谱有 10 种成分。在现代硬件上处理事件应该以微秒为单位,因此读取和重放整个事件日志应该以毫秒为单位。

此外,您是否知道任何显示如何在 Core Data 应用程序中使用事件溯源的示例的链接?[...] 例如,我是否需要摆脱 RecipeHistory NSManagedObject?

我不知道 iOS 上事件溯源的良好参考实现。这与在其他系统上实现它并没有太大的不同。您需要删除当前拥有的所有表,将其替换为如下所示的单个表:

事件日志条目

属性如下:

  • EventId- 此事件的唯一 ID。这是在插入时自动分配的,永远不会改变。
  • EntityId- 此事件创建或修改的实体的唯一 ID。此 ID 由Create...处理器自动分配,并且永远不会更改。
  • EventType- 表示此事件类型名称的短字符串。
  • EventTime- 事件发生的时间。
  • EventData- 事件的序列化表示 - 这可以是二进制或文本的。

最后一项可以替换为表示上述 12 种事件类型使用的属性超集的“非规范化”列组。这完全取决于您 - 此表只是存储您的事件的一种可能方式。它不必是核心数据——事实上,它甚至不需要在数据库中(尽管它使事情变得更容易一些)。

于 2013-11-24T00:43:09.853 回答
4

I think when a row in RecipesHistoryViewControlleris selected to modification, we can optimize the Saveprocess with two options:

  • 让用户选择是否必须保存新行或可能发生更新。
    有一个Save New用于创建新行的按钮Recipe和一个Update用于更新当前选定行的按钮。
  • 为了跟踪对配方所做的更改(发生更新时),我将尝试仅记录配方的更改。
    使用EAV模式将是一种选择。

    在此处输入图像描述]![在此处输入图像描述]![在此处输入图像描述 提示:
    成分名称的逗号分隔值可以用作旧值和新值,当在 RecipeHistory 表中插入一行时,示例可能会有所帮助。

关于大更新
假设实际应用程序有一个用于持久操作的数据库,一些建议可能会有所帮助。

在第一部分找到的当前配方和在“历史”部分找到的 X 配方没有任何共同之处

Current导致与配方之间没有关系的自然方式In-History,因此试图建立关系将是徒劳的。如果没有关系,设计就不会是正常形式,冗余将是不可避免的。
流动的方法会有很多记录,在这种情况下

  • 我们可以将任何用户保存的食谱限制在预定义的数量内。

  • 另一种优化配方表性能的解决方案是根据创建日期字段对表进行范围分区(让数据库管理员参与其中)。

  • 另一个建议是为成分概念设置一个单独的表格。
    拥有ingredient, recipe,recipe-ingredient 表将减少冗余。
    在此处输入图像描述

使用 NoSql
如果关系不是应用程序逻辑的重要部分,我的意思是,如果您不会以复杂的查询结束,例如“哪些成分在少于总 Y 成分的食谱中使用超过 X 次,而牛奶不是其中之一”或分析程序然后,看看NoSql数据库和它们的比较

它们提供非关系、分布式、开源、无模式、简单的复制支持、简单的 API、海量数据和水平可扩展性。

有关基于文档的数据库的基本示例:
拥有couchdb安装在我的本地机器上(端口号 5984)在 couchdb 上创建配方数据库(表)将通过发送标准 HTTP 请求(使用 curl)来完成,例如:

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

删除配方表:

curl -X DELETE http://127.0.0.1:5984/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" 
],}'

获取 myFirstRecipe 记录(文档)

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

不需要像对象关系映射、数据库驱动程序等经典的服务器端过程,
顺便说一句,使用 Nosql 会有你需要考虑的缺点,比如这里这里

于 2013-11-21T09:41:22.957 回答
2

有几个问题需要回答:

  1. 食谱的“历史项目”数量是否有限制,还是真的有必要保留食谱的所有版本?
  2. 何时修改只是对现有配方的更改,何时更改会导致新配方?例如,是否应该允许用户通过完全替换所有成分和标题来将“芝士蛋糕”食谱更改为“肉饼”食谱?

在规划数据模型时,这些问题的答案很重要。例如,问问自己这是否是您的应用程序的有效用例:用户创建了一个包含糖、面粉和鸡蛋的“基本蛋糕”食谱。用户现在想要将此“基本蛋糕”食谱作为模板来创建“芝士蛋糕”、“磅蛋糕”和“胡萝卜蛋糕”食谱。这是一个有效的用例吗?

如果是这样,每次您保存食谱时,它基本上都会创建一个全新的、独立的食谱,因为用户可以更改所有内容,从而将芝士蛋糕变成肉饼。

但是,我认为这对用户来说是意想不到的行为。在我看来,用户创建了一个“芝士蛋糕”食谱,然后可能想要跟踪对该食谱的更改,而不是将其变成完全不同的东西。

这就是我的建议:

  1. 而不是RecipeHistory拥有Recipes,更改您的数据模型,以便Recipes拥有多个RecipeVersions. 这样,用户可以明确地创建新配方,然后跟踪对该配方的更改。此外,不允许用户RecipeVersion直接编辑 a,而是可以将他们的配方“恢复”到特定版本,然后对其进行编辑。
  2. 独一无二: “ IngredientsButter”、“Milk”和“Flour”在数据库中只存在一次,只是不同配方的引用。这样,您的数据库中将不会有重复项,并且仅保存引用将占用更少的磁盘空间,而不是一次又一次地保存成分的名称。
  3. 允许您的用户基于现有配方(版本)创建新配方。这样,您就可以让您的用户在现有配方的基础上“建立”新配方,而不会使您的应用程序和数据模型复杂化。

这是我建议的数据模型:

+----- Recipe ------+
| attributes:       |
| - name            |
| relationships:    |
| - recipeVersions  |
+-------------------+

+-- RecipeVersion  ----+
| attributes:          |
| - timestamp          |
+----------------------+
| relationships:       |
| - recipe             | 
| - ingredients        |
+----------------------+

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

享受。

于 2013-11-19T17:03:43.850 回答
2

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.

于 2013-11-19T04:33:54.423 回答
2

您不需要复制所有的成分对象。相反,只需更改关系,使食谱具有许多成分,并且成分可以在许多食谱中。然后,当您创建重复配方时,您只需连接到现有成分。

这也将使列出使用某种(或某种组合)成分的食谱变得更加容易。

你还应该考虑你的 UI/UX——它应该是一个完整的副本吗?或者您是否应该允许用户在每个配方中创建“替代品”(仅列出一组替代成分)。

于 2013-11-21T20:03:11.530 回答
1

可能我没看懂你的问题,但是你需要通过编辑来更改黄油的名称吗?为什么不从那个食谱中删除黄油并添加花生酱呢。这样,您就不会在其他与之相关的食谱中将黄油更改为花生酱?使用新食谱,您可以选择花生酱或黄油。

于 2013-11-24T13:44:14.117 回答
1

不确定我是否清楚您要解决的问题,但我会首先对配方和成分进行建模,并将它们与可能随着烹饪实验而改变的实际混合和方法分开。使用一些智能应用程序逻辑,您只能跟踪每个版本中的更改,而不是制作新副本。例如,如果用户决定尝试新版本的配方,则默认显示以前的版本(或允许用户选择一个版本)方法和配方成分,如果进行了任何更改,则将这些更改保存为与关联的新方法和配方成分食谱版本。

这种方法将使用更少的存储空间,但需要更复杂的应用程序逻辑,例如交换一种成分会将被替换的成分的数量设置为 0,并为新的成分添加新记录。简单地复制以前的(或用户选择的)版本不会占用太多空间,这些都是小记录,实现起来会简单得多。

在此处输入图像描述

于 2013-11-20T23:47:46.353 回答
1

这是存储大小和检索时间之间的权衡。

如果每次用户单击“保存配方”按钮时都复制每个配方,则会在数据库中复制大量数据。

如果您创建一个包含 Recipe 和更改列表的 RecipeHistory 对象,则检索数据和填充 View Controller 需要更长的时间,因为您必须在内存中重建完整的 Recipe。

我不确定哪个更容易 - 适合您的用例可能是最好的。

于 2013-11-15T03:32:21.780 回答
1

我相信最好将成分表定义为具有成分 ID 和成分显示名称,并在接收者历史表中存储 RecipieID、HistoryDate、IngredientArray。

如果在成分表中,id:1 是黄油 id:2 是牛奶 id:3 是奶酪 id:4 是糖 id 5 是豆浆

然后在食谱 1 的历史表中:芝士蛋糕,11 月 15 日的数据,成分数组:{1,2,3,4} 如果在 11 月 16 日芝士蛋糕更改为豆浆而不是牛奶,那么在该日期成分数组为 {1,2 ,3,5} 。许多数据库具有数组列选项,或者可以是逗号分隔的字符串或 Json 文档。最好将成分列表保存在内存中以快速查找以从列表中获取成分名称。

于 2013-11-24T12:04:03.450 回答
0

为了清楚起见,我们在谈论前端?

首先,就像 Mohsen Heydari 建议的那样,在 SQL rdbms 上,您应该在多对多连接之间创建一个表,以实现两个一对多的性能。

所以你想要一个历史性的

+-- RecipeHistory --+
|                   |
| attributes:       |
| - id              |
| - date            |
| - new name?       |
| - notes ??        |
| - recipe-id       |
+-------------------+
| relationships:    |
| - recipes         |
+-------------------+

+----- Recipe ------+
| attributes:       |
| - id              |
| - name            |
| - discription     |
| - date            |
| - notes           | #may be useful?
| - Modifiable      | #this field is false if in history, else true,
+-------------------+
| relationships:    |
| recipe-ingredient |
+-------------------+

+-Recipe-ingridient-+
| attributes:       |
| id                |
| recipe-id         |
| ingridient-id     |
| quantity          |
+-------------------+

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

现在,如果配方上的可修改字段 = True 它属于 MainPage

如果为假,则属于历史页面

找到您想要的食谱后,您可以使用 Recipe-Ingredient 表通过其食谱 ID 查询成分,或以相同的方式按成分查询食谱。

另一个减少空间消耗的选项是创建一个配方历史,并创建一个修改后的配方表 -> 它采用基本配方 ID,

并将其映射到 -> Main Recipe ID, Discarded Ingredients and New Ingredients,如果您想解释这个解决方案,请询问

于 2017-09-10T07:38:09.557 回答