Core Data Model Design
让我们假设我有一个关于烹饪食谱的应用程序,它有两个基本特征:
标准情景
我目前的食谱是"芝士蛋糕",在
- 糖
- 牛奶
- 牛油
- 等等
好吧,让我说我对最终结果感到满意,我决定保存(记录)我刚准备好的食谱。
*点击保存*
配方现在已保存(现已记录),在
- 2013年11月15日 - 芝士蛋糕
- 2013年11月11日 - 布朗尼
- 等等
现在,如果我想,我可以编辑历史中的食谱,并将牛奶换成豆浆。
在历史记录中编辑配方的问题不应该在我当前的配方中编辑配方(及其成分),反之亦然。如果我编辑当前食谱并用花生酱替换黄油,则不得编辑历史记录中存储的任何食谱。希望我解释自己。
后果
这种情况意味着什么?意味着目前,为了满足这些功能的功能,每次用户点击"保存配方"按钮时,我都会复制配方和每个子关系(成分)。它确实有效,但我觉得它可以更干净。有了这个实现,事实证明我有不同重复的核心数据对象(sqlite行)的TONS,如下所示:
- 对象#1,名称:黄油,食谱:1
- 对象#2,名称:黄油,食谱:4
- 对象#3,名称:黄油,食谱:3
等等
想法?如何优化此模型结构?
编辑1
我已经想过用属性
编辑2
目前,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | +-- RecipeHistory --+ | | | attributes: | | - date | +-------------------+ | relationships: | | - recipes | +-------------------+ +----- Recipe ------+ | relationships: | | - recipeInfo | | - recipeshistory | | - ingredients | +-------------------+ +-- RecipeInfo ----+ | | | attributes: | | - name | +-------------------+ +--- Ingredient ----+ | | | attributes: | | - name | +-------------------+ | relationships: | | - recipe | +-------------------+ |
paulrehkugler是真的,当他说我创建一个
大新闻
我已经阅读了一些用户的答案,我想更好地解释一下这种情况。
我上面说的例子只是一个例子,我的意思是我的应用程序不涉及烹饪/食谱参数,但我使用了食谱,因为我认为它对我的真实场景非常好。
说这个我想解释一下应用程序需要两个部分:
- 第一:我可以在哪里看到有相关成分的CURRENT配方
- 第二:我可以通过点击第一部分中的"保存配方"按钮来查看配方
在第一部分中找到的当前配方和在"历史"部分中找到的X配方没有共同的缺点。然而,用户可以编辑"历史"部分中保存的任何食谱(他可以编辑名称,成分,他想要的任何东西,他可以完全编辑历史部分中找到的食谱的所有内容)。
这就是为什么我复制所有
无论如何我不喜欢这种方法,即使它似乎是唯一的一种。问我想要什么,我会尽力解释每一个细节。
PS:对不起我的基础英语。
编辑
由于您必须处理历史记录,并且因为事件是由最终用户手动生成的,因此请考虑更改方法:而不是存储模型实体的当前视图(即配方,成分和它们之间的连接)存储启动的各个事件由用户。这称为事件采购。
这个想法是记录用户的行为,而不是记录用户操作后的新状态。当您需要获取当前状态时,"重放"事件,将更改应用于内存中的结构。除了让您实现即时要求之外,还可以通过"重播"事件直到特定日期来恢复特定日期的状态。这有助于审计。
你可以通过定义这样的事件来做到这一点:
-
CreateIngredient - 添加新成分,并为其提供唯一ID。 -
UpdateIngredient - 更改现有成分的属性。 -
DeleteIngredient - 从当前状态删除成分。删除配料会从所有配方和配方历史中删除它。 -
CreateRecipe - 添加新配方,并为其提供唯一ID。 -
UpdateRecipeAttribute - 更改现有配方的属性。 -
AddIngredientToRecipe - 将成分添加到现有配方中。 -
DeleteIngredientFromRecipe - 从现有配方中删除配料。 -
DeleteRecipe - 删除食谱。 -
CreateRecipeHistory - 从特定配方创建新配方历史记录,并为历史记录提供新ID。 -
UpdateRecipeHistoryAttribute - 更新特定配方历史记录的属性。 -
AddIngredientToRecipeHistory - 将配料添加到配方历史记录中。 -
DeleteIngredientFromRecipeHistory - 从食谱历史中删除成分。
您可以使用Core Data API将单个事件存储在单个表中。添加按顺序处理事件的类,并创建模型的当前状态。这些事件将来自两个地方 - 由Core Data支持的事件存储,以及来自用户界面。这将允许您保留单个事件处理器和单个模型,其中包含当前配方,成分和配方历史状态的详细信息。
Replaying the events should happen only when the user consults the history, right?
不,这不是发生的事情:您将启动时的整个历史记录读入当前的"视图",然后将新事件发送到视图和数据库以进行持久化。
当用户需要查阅历史记录时(特别是当他们需要了解模型在过去的特定日期时的外观时),您需要部分重播事件,直到感兴趣的日期。
由于事件是手工生成的,因此不会有太多:我估计最多数千个 - 这是100个食谱的清单,每个食谱有10种成分。在现代硬件上处理事件应该在几微秒内,因此读取和重放整个事件日志应该在几毫秒内。
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?
我不知道iOS上的事件源代码的良好参考实现。这与在其他系统上实现它没有什么不同。您需要摆脱当前所有的表,将其替换为如下所示的单个表:
属性如下:
-
EventId - 此活动的唯一ID。这是在插入时自动分配的,永远不会更改。 -
EntityId - 此事件创建或修改的实体的唯一ID。此ID由Create... 处理器自动分配,永不更改。 -
EventType - 表示此事件类型名称的短字符串。 -
EventTime - 事件发生的时间。 -
EventData - 事件的序列化表示 - 可以是二进制或文本。
最后一项可以替换为"非规范化"列组,表示上述12种事件类型使用的属性的超集。这完全取决于您 - 此表仅是存储事件的一种可能方式。它不一定是核心数据 - 事实上,它甚至不需要在数据库中(尽管它使事情变得容易一些)。
我认为当选择
-
让用户选择是否必须保存新行或可能发生更新。使用
Save New 按钮在Recipe 中创建新行,使用Update 按钮更新当前所选行。 -
为了跟踪对配方的更改(更新发生时),我将尝试仅记录配方的更改。使用EAV模式将是一种选择。
作为提示:成分名称的逗号分隔值可以用作旧值和新值
在RecipeHistory表中插入一行,样本可能有所帮助。
关于BIG UPDATE:
假设真实应用程序具有用于持久操作的数据库,一些建议可能会有所帮助。
The current recipe found in the first section and a X recipe found in
the 'history' section doesn't have NOTHING in common
导致
试图建立关系将是徒劳的。没有任何关系,设计将不会处于正常状态,冗余将是不可避免的。在这种情况下会有很多记录,在这种情况下
-
我们可以使用预定义的数字限制任何用户保存的食谱。
-
优化配方表性能的另一种解决方案是范围
根据创建日期字段对表进行分区(让数据
基地管理员参与)。 -
另一个建议是有一个单独的成分表
概念。有ingredient ,Recipe ,recipe-ingredient
表将减少冗余。
使用NoSql
如果关系不是应用程序逻辑的微不足道的一部分,我的意思是如果你不会在复杂的查询中结束,例如"哪些成分在含有少于总Y成分的配方中被使用超过X次而且牛奶不是然后,他们"或分析程序,看看NoSql数据库并比较它们。
它们提供非关系,分布式,开源,无架构,易于复制支持,简单的API,大量数据和水平可扩展。
有关基于文档的数据库的基本示例:在我的本地计算机上安装couchdb(端口号5984),在couchdb上创建配方数据库(表)将通过发送标准HTTP请求(使用curl)来完成,如:
1 | curl -X PUT http://127.0.0.1:5984/recipe |
丢弃食谱表:
1 | curl -X DELETE http://127.0.0.1:5984/recipe |
添加食谱:
1 2 3 4 5 6 | 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记录(文档)
1 | curl -X GET http://127.0.0.1:5984/recipe/myFirstRecipe |
不需要像对象关系映射,数据库驱动程序等经典的服务器端进程
BTW使用Nosql会有你需要考虑的缺点,比如这里和这里。
您不需要复制所有成分对象。相反,只需改变关系,使配方有许多成分和成分可以在许多食谱中。然后,当您创建一个重复的配方时,您只需连接到现有的配料。
这也可以更容易地列出使用(或某些组合)成分的配方。
您还应该考虑您的UI / UX - 它应该是完整的副本吗?或者您应该允许用户在每个配方中创建"替代品"(仅列出一组替代成分)。
有几个问题需要回答:
在规划数据模型时,这些问题的答案非常重要。例如,问问自己这是否是您的应用程序的有效用例:用户创建一个包含糖,面粉和鸡蛋的"基本蛋糕"食谱。用户现在想要将这个"基本蛋糕"食谱作为模板来创建"芝士蛋糕","磅蛋糕"和"胡萝卜蛋糕"食谱。这是一个有效的用例吗?
如果是这样,每次保存配方时,它基本上都会创建一个全新的独立配方,因为允许用户更换所有内容,从而将芝士蛋糕变成肉块。
但是,我认为这对用户来说是意想不到的行为。在我看来,用户创建了一个"芝士蛋糕"食谱,然后可能想跟踪那个配方的变化而不是把它变成完全不同的东西。
这就是我的建议:
这是我建议的数据模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | +----- Recipe ------+ | attributes: | | - name | | relationships: | | - recipeVersions | +-------------------+ +-- RecipeVersion ----+ | attributes: | | - timestamp | +----------------------+ | relationships: | | - recipe | | - ingredients | +----------------------+ +--- Ingredient ----+ | attributes: | | - name | +-------------------+ | relationships: | | - recipeVersions | +-------------------+ |
请享用。
在我看来,你的问题比模型结构更具概念性
我对你的模型的想法是:
+ ******* +
配方
-----------------
-----------------
属性:
-----------------
- isDraft - BOOL
- 名称 - NSString
- creationDate - NSDate
-----------------
-----------------
关系:
-----------------
- 成分 - 与成分对多
-----------------
+ ******* +
+ ******* +
成分
-----------------
-----------------
属性:
-----------------
- 名称 - NSString
-----------------
-----------------
关系:
-----------------
- 食谱 - 与食谱的许多人
-----------------
+ ******* +
现在,让我们称你的"当前"食谱为草稿(用户可能有很多草稿)
如您所见,您现在可以使用单个提取结果控制器(FRC)显示您的配方
获取请求将如下所示:
1 2 3 4 5 | NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:@"Recipe"]; [r setFetchBatchSize:25]; NSSortDescriptor* sortCreationDate = [NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]; [r setSortDescriptors:@[sortCreationDate]]; |
您可以在
1 2 3 4 | NSFetchedResultsController* frc = [[NSFetchedResultsController alloc] initWithFetchRequest:r managedObjectContext:context sectionNameKeyPath:@"isDraft" cacheName:nil]; |
请务必为您的部分提供适当的标题,以免混淆用户
现在,您剩下的就是添加一些特定的功能,如:
-
创造新的食谱
- 保存
- 保存草稿
-
编辑食谱(草稿与否)
- 如果草案提供保存为完整的食谱
- 否则,保存实际的食谱
- 如果您愿意,可以添加"另存为"选项
- 创建副本(用户知道如果他多次保存相同的配方,他可能会引入冗余数据)
无论如何,用户体验应该是一致的
含义:
当用户正在编辑/添加对象时,该对象不应该"在他的脚下"改变
如果用户正在添加新配方,那么他可能希望将其保存为草稿或完整配方
当他保存时,在任何一种情况下,他可能仍希望继续编辑它。因此,不需要创建新对象
如果您想为配方添加版本控制,则需要添加与单个配方相关的
您可以根据需要序列化并存储数据
所以你可以看到,它更像是一个概念问题(你如何访问你的数据),而不是一个建模问题
也许我不明白你的问题,但是你需要通过编辑改变黄油的名称吗?为什么不从那个配方中删除黄油并加入花生酱。这样你就不会把黄油变成花生酱,而不是你的其他配方吗?使用新食谱,您可以选择花生酱或黄油。
我认为最好将成分表定义为具有ingredientID和ingredientDisplayName,并在recipie历史表中存储RecipieID,HistoryDate,IngredientArray。
如果在配料表中,
id:1是黄油
id:2是牛奶
id:3是奶酪
id:4是糖
id 5是豆浆机
然后在历史表中
食谱1:芝士蛋糕,数据11月15日,IngredientArray:{1,2,3,4}
如果在11月16日芝士蛋糕改变了豆奶而不是牛奶那么在那个日期,IngredientArray是{1,2,3,5}。许多数据库都有数组列选项,或者可以是逗号分隔的字符串或Json文档。
最好将成分列表保存在内存中以快速查找以从列表中获取成分名称。
不确定我是否清楚你要解决的问题,但我会先从配方和配料的模型开始,并将它们与实际的混合物和方法分开,这可能会随着厨师的实验而改变。使用一些智能应用程序逻辑,您只能跟踪每个版本中的更改,而不是创建新副本。例如,如果用户决定尝试新版本的配方,则默认显示以前的版本(或允许用户选择版本)方法和RecipeIngredients,如果进行了任何更改,请将这些更改保存为与方法和RecipeIngredient关联的新方法RecipeVersion。
这种方法将使用更少的存储空间,但需要更复杂的应用程序逻辑,例如,交换成分会将数量设置为0以替换被替换的数据并为新的数据添加新记录。简单地复制先前(或用户选择的)版本不会占用太多空间,这些是小记录,并且实现起来要简单得多。
这是存储大小和检索时间之间的权衡。
如果每次用户单击"保存配方"按钮时复制每个配方,则会复制数据库中的大量数据。
如果您创建具有配方和更改列表的RecipeHistory对象,则需要更长时间来检索数据并填充视图控制器,因为您必须在内存中重建完整的配方。
我不确定哪个更容易 - 哪个适合你的用例可能是最好的。
为了清楚起见,我们正在谈论前端?
首先,就像Mohsen Heydari对SQL rdbms的建议一样,你应该在多对多连接之间创建一个表,以便为性能做出两个一对多。
所以你想要一个历史性的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | +-- 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| +-------------------+ |
现在,如果Recipe上的可修改字段= True,则它属于MainPage
如果它是假的,它属于历史页面
在找到您想要的食谱后,您可以使用Recipe-Ingredient表或Recipe by Ingredients以相同的方式通过其recipe-id查询成分。
另一个选择空间较少的选项是创建配方历史记录,并创建一个修改的配方表 - >其中包含一个基本配方ID,
并将其映射到 - >主食谱ID,废弃成分和新成分,如果您想要解释此解决方案,请询问