关于ios:核心数据模型设计

Core Data Model Design

让我们假设我有一个关于烹饪食谱的应用程序,它有两个基本特征:

  • 第一个涉及我正在准备的CURRENT配方
  • 第二个存储我决定保存的食谱
  • 标准情景

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

    • 牛奶
    • 牛油
    • 等等

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

    *点击保存*

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

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

    现在,如果我想,我可以编辑历史中的食谱,并将牛奶换成豆浆。

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

    后果

    这种情况意味着什么?意味着目前,为了满足这些功能的功能,每次用户点击"保存配方"按钮时,我都会复制配方和每个子关系(成分)。它确实有效,但我觉得它可以更干净。有了这个实现,事实证明我有不同重复的核心数据对象(sqlite行)的TONS,如下所示:

    • 对象#1,名称:黄油,食谱:1
    • 对象#2,名称:黄油,食谱:4
    • 对象#3,名称:黄油,食谱:3

    等等

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

    编辑1

    我已经想过用属性NSString创建任何RecipeHistory对象,我可以存储一个json字典,但我不知道它是否更好。

    编辑2

    目前,RecipeHistory对象包含:

    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是真的,当他说我创建一个RecipeHistory时会复制每个Recipe对象(及其关系RecipeInfo和Ingredients)将用大量数据填充数据库但我找不到另一个允许我的解决方案未来的灵活性。也许将来我会创建有关食谱和历史的统计数据,并且核心数据对象可能会被证明是有用的。你怎么看?我认为这是存储历史记录并允许编辑历史记录项的许多应用程序中的常见场景。

    大新闻

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

    说这个我想解释一下应用程序需要两个部分:
    - 第一:我可以在哪里看到有相关成分的CURRENT配方
    - 第二:我可以通过点击第一部分中的"保存配方"按钮来查看配方

    在第一部分中找到的当前配方和在"历史"部分中找到的X配方没有共同的缺点。然而,用户可以编辑"历史"部分中保存的任何食谱(他可以编辑名称,成分,他想要的任何东西,他可以完全编辑历史部分中找到的食谱的所有内容)。

    这就是为什么我复制所有NSManagedObject的原因。然而,通过这种方式,数据库将变得疯狂,因为每次用户保存当前配方时,表示配方(Recipe)的对象都是重复的,并且还有配方所具有的关系(成分)。因此,将会有一些名为"黄油"的成分。你可以这么说:为什么你需要拥有"黄油"物品?好吧,我需要它,因为成分具有例如'数量'属性,因此每个配方都有不同数量的成分。

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

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

    编辑

    enter image description here


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

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

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

    • 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上的事件源代码的良好参考实现。这与在其他系统上实现它没有什么不同。您需要摆脱当前所有的表,将其替换为如下所示的单个表:

    Event log entry

    属性如下:

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

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


    我认为当选择RecipesHistoryViewController中的行进行修改时,我们可以使用两个选项优化Save进程:

    • 让用户选择是否必须保存新行或可能发生更新。使用Save New按钮在Recipe中创建新行,使用Update按钮更新当前所选行。
    • 为了跟踪对配方的更改(更新发生时),我将尝试仅记录配方的更改。使用EAV模式将是一种选择。
      enter image description here]![enter image description here]![enter image description here
      作为提示:成分名称的逗号分隔值可以用作旧值和新值
      在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

    导致CurrentIn-History配方之间没有关系的自然方式,所以
    试图建立关系将是徒劳的。没有任何关系,设计将不会处于正常状态,冗余将是不可避免的。在这种情况下会有很多记录,在这种情况下

    • 我们可以使用预定义的数字限制任何用户保存的食谱。

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

    • 另一个建议是有一个单独的成分表
      概念。有ingredientReciperecipe-ingredient
      表将减少冗余。enter image description here

    使用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 - 它应该是完整的副本吗?或者您应该允许用户在每个配方中创建"替代品"(仅列出一组替代成分)。


    有几个问题需要回答:

  • 食谱的"历史项目"的数量是否有限制,或者是否真的有必要保留食谱的所有版本?
  • 何时进行修改只是更改现有配方,何时更改会导致新配方?例如,是否允许用户通过完全替换每种成分和标题将"芝士蛋糕"配方更改为"肉块"食谱?
  • 在规划数据模型时,这些问题的答案非常重要。例如,问问自己这是否是您的应用程序的有效用例:用户创建一个包含糖,面粉和鸡蛋的"基本蛋糕"食谱。用户现在想要将这个"基本蛋糕"食谱作为模板来创建"芝士蛋糕","磅蛋糕"和"胡萝卜蛋糕"食谱。这是一个有效的用例吗?

    如果是这样,每次保存配方时,它基本上都会创建一个全新的独立配方,因为允许用户更换所有内容,从而将芝士蛋糕变成肉块。

    但是,我认为这对用户来说是意想不到的行为。在我看来,用户创建了一个"芝士蛋糕"食谱,然后可能想跟踪那个配方的变化而不是把它变成完全不同的东西。

    这就是我的建议:

  • 而不是RecipeHistory拥有Recipes,而是更改数据模型,以便Recipes具有多个RecipeVersions。这样,用户可以显式创建新配方,然后跟踪对该配方的更改。此外,不允许用户直接编辑RecipeVersion,而是可以将其配方"恢复"为特定版本,然后进行编辑。
  • 使Ingredients唯一:"黄油","牛奶"和"面粉"在数据库中只存在一次,并且仅由不同的食谱引用。这样,您不会在数据库中存在重复项,只保存引用将占用较少的磁盘空间,而不是一次又一次地保存该成分的名称。
  • 允许您的用户根据现有配方(版本)创建新配方。这样,您就可以让用户在现有的配方上"建立"新配方,而不会使您的应用和数据模型复杂化。
  • 这是我建议的数据模型:

    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]];

    您可以在isDraft属性上划分数据:

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

    请务必为您的部分提供适当的标题,以免混淆用户

    现在,您剩下的就是添加一些特定的功能,如:

    • 创造新的食谱

      • 保存
      • 保存草稿
    • 编辑食谱(草稿与否)

      • 如果草案提供保存为完整的食谱
      • 否则,保存实际的食谱
      • 如果您愿意,可以添加"另存为"选项
    • 创建副本(用户知道如果他多次保存相同的配方,他可能会引入冗余数据)

    无论如何,用户体验应该是一致的
    含义:

    当用户正在编辑/添加对象时,该对象不应该"在他的脚下"改变
    如果用户正在添加新配方,那么他可能希望将其保存为草稿或完整配方
    当他保存时,在任何一种情况下,他可能仍希望继续编辑它。因此,不需要创建新对象

    如果您想为配方添加版本控制,则需要添加与单个配方相关的RecipeHistory实体。此实体将记录完整配方对象中每个已提交更改的更改(使用NSManagedObject NSManagedObject或检查现有/已提交的值)。
    您可以根据需要序列化并存储数据

    所以你可以看到,它更像是一个概念问题(你如何访问你的数据),而不是一个建模问题


    也许我不明白你的问题,但是你需要通过编辑改变黄油的名称吗?为什么不从那个配方中删除黄油并加入花生酱。这样你就不会把黄油变成花生酱,而不是你的其他配方吗?使用新食谱,您可以选择花生酱或黄油。


    我认为最好将成分表定义为具有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以替换被替换的数据并为新的数据添加新记录。简单地复制先前(或用户选择的)版本不会占用太多空间,这些是小记录,并且实现起来要简单得多。

    enter image description here


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

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

    如果您创建具有配方和更改列表的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,废弃成分和新成分,如果您想要解释此解决方案,请询问