问题回顾
正如@leanne 所说,您正在为Subscription 建模,其专长是MonthlySubscription 和ComplimentarySubscription(为这个答案命名)。
您知道订阅可能会过期:
- 对于
MonthlySubscription,当用户没有支付当月的订阅费用时会发生这种情况
- 对于
ComplimentarySubscription,到期日期是在分配给用户时分配的
如您所见,ExpirationDate 是任何Subscription 的基本属性,但在每种情况下存储它的方式都不同。如果第一种情况你必须根据最后一个事件计算它,在后者你可以直接检索它。
处理数据库中的继承
因此,要将这个示例模型映射到数据库模式,您可以使用 Martin Fowler 的 Patterns of Enterprise Application Architecture 书中描述的 Class Table Inheritance 模式。这是它的意图:
“表示类的继承层次结构,每个类一个表”。
基本上,您将拥有一个表,其中包含在类之间共享的属性,并且您会将特定于每个类的属性存储在单独的表中。
记住这一点,让我们回顾一下您提出的选项:
- 是否有另一个表
complimentary_subscription 以用户ID 作为外键?
拥有一个单独的表来存储ComplimentarySubscription 的具体细节听起来不错,但如果您不将此表与subscription 表相关联,您最终可能会得到一个同时拥有MonthlySubscription 和@ 的用户987654334@。它的外键应该指向subscription 表,该表告诉您用户是否有订阅(您必须为每个用户强制执行最多一个订阅)。
- 在
subscription为他们记录一个特殊的“订阅”?
是的,您必须记录用户每月或免费订阅。但是,如果您正在考虑诸如记录金额为零的特殊订阅之类的事情,那么您正在寻找与您当前设计相匹配的解决方案,而不是为其寻找合适的模型(可能也可能不是)。
- 或者在他们的用户行中为
is_complimentary 和complimentary_expires_date 等列添加另一列?
我个人不喜欢这个,因为你把信息放在不属于它的地方。考虑到这一点,您将在哪里存储免费订阅的到期日期(请记住,您正在计算每月订阅的到期日期,而不是存储到期日期)?似乎所有这些信息都在为自己的“家”而哭泣。
此外,如果稍后您需要添加新类型的订阅,该表将开始变得混乱。
如果您这样做,则每次subscription_event 更改时都必须处理数据同步(如果是按月订阅)。一般来说,我会尽量避免这种数据重复的情况。
示例解决方案
在添加新类型的订阅时,为了支持可扩展性,我会做的是让subscription 表存储MonthlySubscripton 和ComplimentarySubscription 之间的共享详细信息,添加一个type 列键,让您区分一行与哪种订阅相关。
然后,将特定于每个订阅类型的详细信息存储在其自己的表中,并引用父 subscription 行。
为了检索数据,您需要一个对象来负责实例化正确类型的 Subscription,给定 type 行的 type 列值。
您可以查看“企业应用程序架构模式”一书中的模式,以获得有关如何定义 type 列值、如何使用映射器对象执行 Subscription 实例化等方面的进一步帮助开。
2012 年 1 月 3 日更新:定义和处理 type 列的替代方法
这是一个更新,以澄清@enoinoc 在 cmets 中发布的以下问题:
特别是对于建议的 type 列,这可能是指向 Plans 表的外键,该表描述了不同类型的订阅,例如在没有付款的情况下到期前几个月。这听起来合乎逻辑吗?
可以在Plans 表中包含该信息,只要它不是不需要编辑的静态信息。如果是这种情况,请不要过度概括您的解决方案,并将这些知识放在相应的 Subscription 子类中。
关于外键方法,我可以想到这样做的一些缺点:
- 请记住,您的目标是知道
Subscription 的哪个子类用于Plans 表中的每一行。如果你得到的只是一个外键值(比如一个整数),你必须编写代码将该值映射到要使用的类。这意味着你需要额外的工作:)
- 如果您必须做不必要的额外工作,维护可能会很痛苦:每次添加新计划时,您都必须记住在映射代码中硬编码它的外键值。
- 如果数据库导出/导入操作不当,外键可能会发生变化。如果发生这种情况,您的映射代码将不再工作,您将不得不再次部署您的软件(或者,至少是更改的部分)。
建议的解决方案
我要做的是放入Plans 表中的type 列。该列将包含知道如何从特定行构建正确的Subscription 的类的名称。
但是:为什么我们需要一个对象来构建每种类型的Subscription?因为您将使用来自不同表(subscription_event 和 complimentary_subscription)的信息来构建每种类型的对象,隔离和封装这种行为总是好的。
让我们看看Plans 表的样子:
-- 计划表--
标识 |姓名 |类型 |其他栏目...
1 |月刊 | MonthlySubscriptionMapper|
2 |免费 | ComplimentarySubscriptionMapper|
每个SubscriptionMapper 都可以定义一个方法Subscription MapFrom(Row aRow),该方法从数据库中获取一行并为您提供Subscription 子类的正确实例(在示例中为MonthlySubscription 或ComplimentarySubscription)。
最后,要获得在type 列中指定的映射器实例(不使用讨厌的if 或case 语句),您可以从列的值中获取类名,并通过使用反射创建一个该类的实例。