【问题标题】:Trying to avoid "Polymorphic Associations" and uphold foreign key referential integrity试图避免“多态关联”并维护外键参照完整性
【发布时间】:2011-08-30 17:16:23
【问题描述】:

我正在构建一个类似于 Yelp(推荐引擎,但规模较小)的网站,因此系统中将包含三个主要实体:用户、地点(包括企业)和事件。

现在我想知道的是如何为每种类型的实体以及可以应用的每个对象存储照片、cmets 和“赞美”(类似于 Facebook 的“赞”)等信息到(例如,对推荐、照片等发表评论)。现在我这样做的方式是每个人都有一张桌子,即

照片(id、typeowner_id、is_main 等...)
其中 type 表示:1=用户,2=地点, 3=事件

评论(id、object_type、object_id、user_id、content 等...)
其中 object_type 可以是几个不同的对象,例如照片、推荐等

Compliment(object_idobject_type、compliment_type、user_id)
其中 object_type 可以是几个不同的对象,例如照片、推荐等

活动(id、来源、source_typesource_id 等)//for "activity feed"
其中 source_type 是用户、地点或事件

通知(id、收件人、发件人、activity_type、object_typeobject_id 等...)
object_type 和 object_id 将用于提供指向通知对象的直接链接,例如被称赞的用户照片

但是 after reading 一些 posts 在 SO 上,我意识到我无法使用外键保持引用完整性,因为这需要 1:1 关系,并且我的 source_id/object_id 字段可以与 ID 相关联在不止一张桌子上。所以我决定采用保留主要实体的方法,然后将其分解为子集,即

User_Photo (photo_id, user_id) | Place_Photo(photo_id, place_id) |等等……

Photo_Comment (comment_id, photo_id) | Recommendation_Comment(comment_id, rec_id) |等等……

赞美(id,...)//would need to add a surrogate key to Compliment table now

Photo_Compliment(compliment_id, photo_id) | Comment_Compliment(compliment_id, comment_id) |等等……

User_Activity(activity_id, user_id) | Place_Activity(activity_id, place_id) |等等……

我在想我可以创建将每个子表连接到主表的视图以获得我想要的结果。另外,我认为它也适合我在 Code Igniter 中的对象模型。

我想我可以留下的唯一表是通知表,因为有很多对象类型(论坛帖子、照片、推荐等),而且这个表无论如何只能保存一周的通知,所以任何参考完整性问题应该不是什么大问题(我认为)。

那么我会以一种明智的方式来解决这个问题吗?我可能忽略了任何性能、可靠性或其他问题?

我能看到的唯一“问题”是我最终会得到很多表(因为现在我有大约 72 个,所以我想在添加额外的),据我所知,这不是问题。

非常感谢任何形式的反馈。提前致谢。

编辑:为了清楚起见,我不担心我最终会得到另外 10 个左右的桌子。据我所知,表的数量并不是什么大问题(一旦它们被使用)......除非你说 200 左右:/

【问题讨论】:

    标签: mysql database-design comments photo polymorphic-associations


    【解决方案1】:

    此 UoD(话语宇宙)的一些命题

    • 名为 Bob 的用户已登录。
    • 名为 Bob 的用户上传了 56 号照片。
    • 有一个地方叫伦敦。
    • 56 号照片位于伦敦。
    • 名为 Joe 的用户在 56 号照片上创建了“非常好”的评论。

    引入对象 ID

    • 用户 (UserID) 已登录。
    • 用户 (UserID) 上传照片 (PhotoID)。
    • 有地方 (PlaceID)。
    • 照片 (PhotoID) 属于地点 (PlaceID)。
    • 用户 (UserID) 在照片 (PhotoID) 上创建了评论 (CommentID)。

    只是事实类型

    • 用户已登录。
    • 用户上传的照片。
    • 地点存在。
    • 照片是地方。
    • 用户在照片上创建了评论。

    现在提取谓词

    Predicate               Predicate Arity
    ---------------------------------------------
    ... logged in            1 (Unary predicate)
    ... uploaded ...         2 (Binary)
    ... exists               1 (Unary) 
    ... is of ...            2 (Binary)
    ... created ... on ...   3 (Ternary)
    

    看起来每个命题是这个UoD可以用最大三元谓词来表示, 所以我会建议像

    谓词角色 (Role_1_ID, Role_2_ID, Role_3_ID) 是对象在谓词中所扮演的角色。用每个Role_ID 从左到右替换谓词 中的...。 注意只有Role_1_ID是强制的(至少是一元谓词),其他两个可能是NULL。

    在这个简单的模型中,可以提出任何事情。 因此,您需要在 应用层 上实施约束。 例如,您必须确保可以在Place 上创建Comment,但不能在Place 上创建Place。 并非所有的谓词都代表动作,例如... logged in 是动作而... is of ... 不是。 因此,您的活动供稿将列出所有 PropositionsPredicate.IsAction = True

    【讨论】:

    • @Marius Burz;例如? Proposition 表在任意两个资源之间是多对多的。
    • 这是一个非常有趣的 OO 方法,但我认为它对于我的情况可能过于通用。它不会是一个巨大的开放系统,可以添加许多类型的对象(可能会有一些例外情况是可以评论或称赞的,但无论如何这些仍然是有限的)
    • @Ray,越小越容易。
    • @Marius Burz;这将违反 FK Proposition.Role_1_ID。您必须直接删除User 表并将其保留在Resource
    • @Marius Burz;在创建新的用户时,应用程序实际上使用ResourceType = 'usr' 创建了新的Resource,然后使用新生成的ResourceID 插入到User 表中。删除用户时,应用从Resource 中删除,User 中的 FK 具有ON DELETE CASCADE。该应用程序不独立于Resource 处理子类型表。这是标准的超类型/子类型;这里没什么特别的。
    【解决方案2】:

    如果你稍微重新安排一下,你可以简化你的 cmets 和赞美。本质上,您希望拥有一家 cmets 商店和另一家恭维商店。您的问题是这不允许您使用声明性引用完整性(外键约束)。

    解决这个问题的方法是确保可以吸引 cmets 和恭维的对象都是一个超类型的逻辑子类型。从逻辑的角度来看,这意味着您有一个“THING_OF_INTEREST”实体(我不是在这里提出命名约定建议!)并且吸引 cmets 和赞美的各种具体事物中的每一个都将是一个子- THING_OF_INTEREST 类型。因此,您的 cmets 表将有一个“thing_of_interest_id”FK 列,您的恭维表也是如此。您仍将拥有子类型表,但它们将具有 THING_OF_INTEREST 的 1:1 FK。换句话说,THING_OF_INTEREST 的作用是为您提供一个主键域,而所有子类型表都包含特定于类型的属性。这样,您仍然可以使用声明性引用完整性来强制执行您的评论和赞美关系,而不必为不同类型的 cmets 和赞美创建单独的表。

    物理实现的角度来看,最重要的是您感兴趣的各种事物都共享一个共同的主键域。这就是让您的评论表具有单个 FK 值的原因,该值可以轻松地与感兴趣的任何事物相结合。

    根据您对 cme​​ts 和建议的追求,您可能会(但可能不需要)物理实现 THING_OF_INTEREST - 它至少有两个属性,主键(通常是 int)加上一个分区属性,告诉你是哪个子类型的东西。

    【讨论】:

    • 当它们彼此相当相关时(比如questionsanswers 与一个公共表posts),这样的事情对于极少数表来说很有效,但在这种情况下则不然。这感觉就像围绕CommentCompliment 设计系统,使UserPlaceEvent 都从一个共同的父级“下降”。它现在可能有用,但是如果你让自己在路上再得到 10 件有趣的事情呢?它只是变得尴尬,甚至比以大量桌子结束。
    • 如果他们从一个共同的父级中分离出来的唯一意义是父级提供唯一代理主键值的列表,那么从设计的角度来看,它一点也不尴尬,它只能如果这个“PK 井”成为插入热点,则成为物理实现问题。即使这确实发生了,也有办法绕过它。因此,我恭敬地不同意您的观点。
    • 我将不得不在它上面睡觉,这是一个具有挑战性的提议,因为我从来没有想过将它用于这么多桌子。尽管如此,它还是有它的优点,但我仍然必须接受它并“发现”为什么我仍然对它感到保留。
    • @Joel 坐了一天之后,我必须说我可以接受它。我唯一的保留是有一个中心节点(THING_OF_INTEREST),当涉及到扩展时,这非常糟糕。除此之外,你已经得到了我当之无愧的 +1。
    • @marius-burz 我同意在可扩展性方面必须仔细观察这个解决方案,我并不是建议我自己使用这个设计,尽管我已经看到它使用过。我也同意 OP 的观点,即桌子蔓延不应该是一个压倒一切的考虑因素,尽管公平地说,我会指出 OP 的问题确实使用了“问题”这个词,因为它最终得到了很多桌子。与系统设计中的许多事情一样,很少有正确的答案,只有基于所选设计目标和优先级的最佳权衡。
    【解决方案3】:

    如果您需要referential integrity (RI),没有比使用多对多连接表更好的方法了。诚然,您最终会在系统中拥有很多表,但这就是您需要支付的成本。走这条路还有其他一些好处,例如,您可以免费获得某种分区:您可以按关系类型对数据进行分区,每个数据都在自己的表中。这提供了 RI,但它也不是 100% 安全的,例如,没有什么可以保证您的评论属于一张照片并且只属于那张照片,如果您需要它们,您需要手动强制执行这种约束。

    另一方面,使用像您已经做过的通用解决方案可以让您更快地起步,并且在未来更容易扩展,但除非您手动编码,否则不会有 RI(这是非常比每种关系类型的替代 M:M 更复杂且更难处理)。

    只是提到另一个替代方案,类似于您现有的实现,您可以使用自定义 M:M 联结表来处理所有关系,无论它们的类型如何:object1_type, object1_id, object2_type, object2_id。简单但除了很容易实现和扩展之外没有其他好处。如果你不需要 RI 并且你有很多表,我只会推荐它,所有表都是相互关联的。

    【讨论】:

    • 添加多对多交集表根本不能解决 OP 的问题,因为 m:n RI 与 1:m RI 不同。除非 cmets 旨在跨多个照片实例共享,或者 - 正如您自己指出的那样 - 跨不同类型的目标实体共享。此外,在每种情况下都需要三表连接的系统不会像通常可以使用两个表连接的系统(如我所建议的那样)那样高效。您的建议并未解决 OP 对桌子蔓延的明显担忧,也没有解决他对使用 DRI 的渴望。
    • @Joel Brown 我并不真正关心“桌子蔓延”,只是我指出它会增加我已经拥有的数量。我认为一旦表格被使用并有用,那么表格的数量应该不会太大,除非它的数量非常大:/
    • 有句老话听起来像:一个数据库可以根据需要包含尽可能多的表,前提是数据库可以处理这个问题。实际上拥有多个表可能是一个优势,尤其是当数据量很大时:您可以将它们放在不同的磁盘上(如果有意义的话甚至是内存),在每个关系类型的基础上创建索引(在这种情况下)并拥有一个不太中心点(这很重要)。
    • 同意 Marius 的观点。我已经观看了至少 100 个关于 MySQL 可扩展性的 youtube 讲座,所有这些都是关于优化具有大量行的数据库的——介于少数和没有(取决于您如何定义分片等)是关于优化具有大量行的数据库表。
    猜你喜欢
    • 2017-05-27
    • 1970-01-01
    • 2014-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-07
    相关资源
    最近更新 更多