5

我正在构建一个管理应用程序来帮助管理我的移动汽车美容公司(希望还有其他公司)。我正在努力弄清楚如何对一些数据进行建模。

这个问题与我之前发布的一个问题有关,但我已经复制了以下相关信息: Database design - google app engine

在这个应用程序中,有“约会”和“行项目”的概念。

约会是员工为了提供服务而被期望在的地点和时间。

行项目是服务、费用或折扣及其相关信息。可能进入约会的行项目示例:

名称:价格:佣金:时间估算   
完整细节,常规尺寸:160 75 3.5 小时
10 美元的全细节优惠券:-10 0 0 小时
高级细节:220 110 4.5 小时
派生总计(非行项目):370 美元 185 美元 8.0 小时

在我之前的这个应用程序的实现中,行项目包含在一个约会中。这在大多数情况下工作得很好,但有时会引起问题。一个例子是,如果约会中途因为下雨而中断,技术人员必须第二天回来完成。这种情况需要对同一行项目进行两次预约。在这种情况下,我只需将第二次约会的“行项目”设置为“完成”之类的内容,从而稍微捏造数据,然后成本将为 0 美元。

在下一个版本中,我正在考虑启用 Line Items 以匹配多个约会,其表结构如下所示:

Appointment
 start_time
 etc...

Line_Item
 appointment_Key_List
 name
 price
 etc...

这种结构的一个普遍问题是它很复杂,我什至不确定它是否适合将一个行项目与多个约会相匹配。如果行项目只能是一个约会的一部分,那么我实际上可以在每个约会中放置一个行项目列表,当我得到约会时,我已经得到了行项目。

一个更具体的问题是我正在使用谷歌应用程序引擎,如果我想查询一组约会及其相关的行项目,我必须先查询一组约会,然后再查询该行items 使用 IN 运算符测试是否有任何 Line_Item 的约会键落入从先前查询返回的约会键集中。如果我有超过 30 个键需要我对查询进行分片,则第二个查询将失败。我可以对数据进行非规范化以避免这种复杂而广泛的读取查询,并且无论如何我可能不得不在某种程度上进行非规范化,但我宁愿在适当的情况下避免复杂性。

我的问题是这种情况通常是如何建模的?一个行项目与多个约会配对是否合适,或者为每个约会简单地将行项目拆分为单独的项目是否正常,例如“两天工作的第一半”和“两天工作的第二半。” 类似的成功应用程序是如何做到这一点的?在这种情况下,经验法则是什么?哪些实现被证明问题较少?

谢谢!

4

2 回答 2

2

您建议的方法可以正常工作;您可以将订单项的“appointment_Key_list”建模为列表属性,它会按您的预期工作。您不必使用 IN 运算符 - 这是为了将数据存储中的单个值与您拥有的键列表匹配(例如,“WHERE datastore_column IN ('a', 'b', 'c')),而您正在做相反的事情 - 将单个值与数据存储中的列表进行匹配。

不过,我建议反过来可能更适合您的任务:让每个约会都有一个行项目键列表。其操作方式大致相同,但要检索约会的所有数据,您首先要获取约会,然后使用 Appointment 实体中的键对行项目进行批量获取。如果您知道 Appointment 的密钥,那么您就完全无需进行任何查询。

我一直试图向 Pindatjuh 解释为什么查询列表属性的效率不亚于单值属性,但显然需要更详细的描述,所以事不宜迟,这里是......

App Engine 数据存储区索引简介

尽管 Python 和 Java 为数据存储区提供了各种高级接口,但数据存储区本身讲的是一种较低级别的抽象,称为实体。一个实体包括以下内容:

  1. 唯一的主键
  2. (名称,值)对的列表

主键是您已经熟悉的 Datastore 键。(名称,值)对列表是 App Engine 对实体中数据的表示。到目前为止如此简单。具有以下值的实体:

a_string = "Hello, world"
an_int = 123

将被序列化为类似这样的东西:

[('a_string', 'Hello, world'), ('an_int', 123)]

但这如何与列表交互?好吧,列表被视为“乘法”属性。也就是说,具有 n 个项目的列表存储为 n 个单独的属性。一个例子可能会更清楚地说明这一点:

a_string = "Hello, world"
an_int = 123
a_list_of_ints = [42, 314, 9]

将被序列化为:

[('a_string', 'Hello, world'), ('an_int', 123), ('a_list_of_ints', 42), ('a_list_of_ints', 314), ('a_list_of_ints', 9)]

如您所见,列表表示一系列值,所有值都具有相同的名称。当您从数据存储区加载数据时,SDK 会看到重复的值并将其转换为列表。

当它与索引交互时,这一点变得很重要。假设您在“a_string”和“an_int”上有一个索引。当您插入或修改一个值时,App Engine 会为其生成一组索引条目;对于上述索引和上述实体,它会在索引中生成一行,如下所示:

('Hello, world', 123, a_key)

(这里的'a_key' 是原始实体的键的占位符。)当您执行使用此索引的查询时,它只需要在索引上查找具有适当前缀的行(例如,'SELECT * FROM Kind WHERE a_string = "Hello, world" ORDER BY an_int')。

但是,当您为列表编制索引时,App Engine 会插入多个索引行。'an_int' 和 'a_list_of_ints' 上的索引将为上述实体生成这些行:

(123, 42, a_key)
(123, 314, a_key)
(123, 9, a_key)

同样,查询的工作方式与以前相同 - App Engine 只需在索引中查找具有正确前缀的行。列表中的条目数对查询的速度没有影响 - 仅对生成和写入索引条目所需的时间有影响。事实上,查询规划器并不知道 'a_list_of_ints' 是一个多值属性——它只是将它视为任何其他索引条目。

简而言之:

  1. 在索引和查询术语中,包含一个元素的列表和单个属性之间没有实际区别
  2. 索引列表的大小会影响索引所需的时间和空间,但不会影响查询。
  3. 您可以使用简单的相等过滤器执行查询,以匹配列表中具有给定值的任何实体。
于 2010-06-30T18:02:15.723 回答
1

此类问题的通常解决方案是将模型归一化,即第一范式

您的模型采用标准化形式,将有第三个表,其中包含对AppointmentLine_Item行的引用:

Appointment
 start_time
 ...

Line_Item
 name
 price
 ...

Appointment_Line_Item
 appointment_key
 line_item_key

然而有一个问题!由于您使用的是 Google App Engine,并且他们的数据存储区非常有限(“GQL 无法执行类似 SQL 的 JOIN”)并且主要需要非规范化。

您建议使用类似列表的字段。可以使用它,但很难对其进行索引。appointment_key在数据库中每行的列表中搜索一个键 (the ) 并没有真正执行。我提出两种可能:

  1. 重复Line_Item

    Line_Item
     appointment_key
     name
     price
     finished
     ...
    

    ALine_Item应该有一个finished状态,当项目是否由员工完成时。如果员工尚未完成所有行项目,请将其标记为未完成,创建新约会并复制所有未完成的项目。appointment_key您可以在 all 上的字段上建立索引Line_Items,这是一件好事。但是,重复的数据可能是个问题。

  2. 的动态字段Line_Item

    Line_Item
     duplicate_key
     appointment_key
     name
     price
     finished
     ...
    

    创建一个新字段 ,duplicate_keyLine_Item指向另一个Line_Item或 null (保留此键!)。Null 表示Line_Item是原始的,任何其他值表示这Line_ItemLine_Item该字段指向的副本。标记为重复的所有字段都Line_Item继承了原始字段Line_Item,除了appointment_key: 所以它会占用更少的存储空间。此外,该解决方案应该已appointment_key编制索引,以加快查找时间。这需要每个重复查询一个额外的查询Line_Item,这可能是一个问题。

现在,这是一个明确的选择:更好的速度或更好的存储。我会选择第一个,因为它降低了模型的复杂性,并且存储对于现代系统来说从来都不是问题。较低的复杂性通常意味着较少的错误和较少的开发/测试成本,这证明了存储需求的成本是合理的。

于 2010-06-26T19:48:31.930 回答