【问题标题】:Elegant schema to log users' actions记录用户操作的优雅模式
【发布时间】:2011-06-22 22:39:32
【问题描述】:

我有一个数据库架构来记录用户在我的 web 应用中执行的操作:

Log
---
Id
Log_Type_Id
Performed_by_Person_Id
Performed_to_Person_Id
Comment_Id
Story_Id
Photo_Id
etc_Id

Person_Log
----------
Person_Id
Log_Id

通过这种方式,我可以将日志中的条目通知用户,并详细说明究竟发生了什么。问题是日志表必须包含所有可能的用户操作类型(他们修改了故事或评论,创建了故事或评论或照片,更新了个人资料等)。对于每个条目,几乎所有这些字段都必然为空。

理想情况下,我有整个日志引用的单个日志表,可能类似于:

Log
---
Id
Performed_by_Person_Id

Log_Comment
-----------
Id
Log_Id
Comment_Id

Log_Photo
---------
Id
Log_Id
Photo_Id

Person_Log
----------
Person_Id
Log_Id

问题在于,我没有一种简单的方法来通知用户正在发生的与他们有关的事情。我很容易得到他们的日志条目,但是我必须查询每个“子”表以查看细节......我可以将子 Log 表的名称存储在 Log 中,但这似乎很不雅。有没有更好的、更相关的方式来做这件事,也适用于 ORM 系统?

【问题讨论】:

    标签: database database-design notifications relational-database database-schema


    【解决方案1】:

    您的案例看起来像是 Gen-Spec 设计模式的一个实例。 Gen-spec 通过超类-子类层次结构为面向对象的程序员所熟悉。不幸的是,对关系数据库设计的介绍往往会跳过如何为 Gen-Spec 情况设计表。幸运的是,它很好理解。谷歌搜索“关系数据库泛化专业化”将产生几篇关于该主题的文章。或者你可以看看下面的previous discussion

    诀窍在于分配子类(专用)表的 PK 的方式。它不是由某种自动编号功能生成的。相反,它是超类(通用)表中 PK 的副本,因此是对它的 FK 引用。

    因此,如果案例是车辆、卡车和轿车,则每辆卡车或轿车在车辆表中都有一个条目,卡车也将在卡车表中具有一个条目,其 PK 是相应 PK 的副本车辆表。轿车也是如此。只需进行联接就可以很容易地确定车辆是卡车还是轿车,而且您通常希望在这种查询中加入数据。

    【讨论】:

    • 这看起来确实像我们目前采用的方法。但它并没有真正优雅地解决问题......那么我怎么知道车辆是卡车还是轿车?我将不得不查询车辆的每一个“子”表?或者我在 Vehicle 中添加一个字段来指示它是哪种类型(这就是我们所做的)?然后查询是一个两步的过程,我失去了一些关系约束的好处。我认为这不适用于 ORM 系统,但至少 Hibernate 具有以这种方式处理这种情况的工具。无论如何,这看起来是迄今为止最好的方法......
    【解决方案2】:

    这将是引入泛型数据模型的好点,或者至少是跨数据模型的泛型类型系统。这个概念是任何事物都有一个入口,甚至是动作、人员、页面、流程等等。当它到位时,您需要一种在这些实体之间创建任意关系的通用方法,使其之间的链接相当容易。您的问题是我为什么要推广更通用的数据模型而不是我们通常使用的超规范化模型的例子之一。

    我最常使用的模型是 Topic Maps(尽管该信息可能不是最容易理解我在说什么的),其中没有为每个实体提供一个表格,而是有一个包含所有实体的表格,以及一些额外的处理类型化和它们之间的关系。您不必一直这样做,但也许专门将它用于您的用例。这是大约 10 年前的 an article I wrote about it,以及处理特定 RDBMS 视图的 another one by Marc de Graauw

    回到你的问题。使用主题图的示例首先需要表格;

    Topic
    -----
    id
    name
    type
    meta_date_created
    meta_date_created_topic_ref
    meta_date_updated
    meta_date_updated_topic_ref
    meta_date_deleted
    meta_date_deleted_topic_ref
    
    Assoc (relationship)
    --------------------
    id
    type
    
    Assoc member
    ------------
    id
    topic_ref
    role_topic_ref
    

    这将为您提供基础知识(但如果您想全面了解,还有大量的东西需要扩展和实现,例如支持多种类型、持久标识、本体分组,等等,这也是主题地图的一部分),并为您提供 meta_* 字段作为方便的快捷方式,如果这正是您想要的(它们有利于快速搜索:)。

    每个人在“主题”中都有一个条目,例如;

    id: 4572349857
    name: Alexander Johannesen
    type: 12341234
    meta_date_create: {date}
    meta_date_create_topic_ref: 5656
    

    要找出是谁创建了这个用户,请在“主题”中查找 id '5656';

    id: 5656
    name: Billy Bob
    type: 12341234
    

    那是什么类型的呢?在“主题”中查找 id '12341234';

    id: 12341234
    name: Person
    

    这里的概念基础是系统中的每个“事物”(故意模糊;它可以是您想谈论的任何内容)都有一个条目,包括动作;

    id: 34598067
    name: Add new user
    type: 56987  // another topic called 'Action', for example)
    

    通过所有这些,您的日志基本上是通过“Assoc”表在这些实体之间创建关系;

    id: 45673
    type: 45685678
    

    这就是协会本身。 'id' 是什么,并不重要,但类型是(你猜对了)'Topic' 表中的另一个实体;

    id: 45685678
    name: Did action
    

    现在您在“Assoc member”表中填写记录操作的详细信息;

    id: {whatever}
    topic_ref: 5656
    role_topic_ref: 12341234
    

    第一个成员是扮演“Person”角色的 Billy Bob。下一步;

    id: {whatever}
    topic_ref: 34598067
    role_topic_ref: 56987
    

    在这里,“添加新用户”主题扮演“操作”的角色。您可以将此关联扩展到您认为需要的任意数量的项目,例如添加预状态、操作的结果、到目前为止的尝试次数、操作发生的位置(例如,如果它是人们可以执行的功能多页),等等。在 Topic 表中为这些事物创建实体,为它们的关系创建实体,您可以根据需要将其复杂化。

    所有这些乍一看可能有点不协调,但它非常灵活,而且您根本不必为将来的扩展更改数据模型。多年来,我一直在使用这种模型构建系统,对此我只有赞美之词。如果您想走这条路,主题属性的单独表格将遵循关联成员的模型。

    也许可以为这样的较少表的性能提供一个案例,但根据我的经验,大多数 RDBMS 都非常擅长使用内部连接,这是完成这项工作所需的基本工具(所有作为标识符的字段都是明显的索引候选),好在这也与 NoSQL 的思维方式基本兼容,在您和您的数据以及 SQL 和后端想要使用的技术机制之间创建了足够的抽象。

    【讨论】:

    • 这类似于语义 Web 的 RDF。问题是数据集扩展不合理,直到它不能很好地执行,除非你将整个数据集加载到 RAM 中并将其用作对象图。它也不是关系设计,因为没有办法依赖于任何给定实体存在的一组一致的属性。
    • 实际上,this 部分与 RDF(或类似)几乎没有关系。 RDF 在级联中使用三元组来表示属性,而这是一个从一开始就是有向图(对术语感到抱歉:),因此我们可以直接使用有用的数据模型,而不必将其包装在“框架”中或“引擎。” :)
    • 所以它是一个网络模型? en.wikipedia.org/wiki/Network_model_%28database%29
    • 嗯,不,它更接近(并且当然兼容)图模型 (en.wikipedia.org/wiki/Graph_database)。但是为了获得原始问题的基础知识,我只谈论其中的一部分,而不是完整的实现。例如,具有类型的事物中有很多隐藏的魔力(查询与类型 Y 的主题有关系的所有 X 类型的事物非常强大),但在这里我只展示了每个主题的一个类型,用于实用接近。
    • 您能否按照 OP 的要求,推荐一种 ORM,使其能够以关系方式轻松使用图模型?
    【解决方案3】:

    我确实推荐您描述的第二种设计。如果要获取每个日志子类型表的所有列,可以使用 LEFT OUTER JOIN:

    SELECT *
    FROM Person_Log AS p
    INNER JOIN Log AS l ON p.Log_ID = l.Log_ID
    LEFT OUTER JOIN Log_Comment AS lc ON l.Log_Type = 'C' AND l.Log_ID = lc.Log_ID
    WHERE p.Person_ID = 1234;
    

    在一个查询中对所有日志类型执行此操作不是一个好主意,因为如果您有多个给定类型的日志条目,则会导致笛卡尔积。因此,请为每个日志子类型单独查询。

    您还可以使用约束,以便确保所有表中只有一个子类型行引用 Log 中的给定行:

    Log
    ---
    Log_Id
    Log_Type constrained to ('C', 'P', etc.)
    Performed_by_Person_Id
    UNIQUE KEY (Log_Id,Log_Type)
    
    Log_Comment
    -----------
    Log_Id PRIMARY KEY
    Log_Type constrained to only 'C'
    Comment_Id
    FOREIGN KEY (Log_Id,Log_Type) REFERENCES Log(Log_Id,Log_Type)
    
    Log_Photo
    ---------
    Log_Id PRIMARY KEY
    Log_Type constrained to only 'P'
    Photo_Id
    FOREIGN KEY (Log_Id,Log_Type) REFERENCES Log(Log_Id,Log_Type)
    

    你的评论:

    这与@Walter Mitty 提到的gen-spec 设计基本相同。

    它也与 Martin Fowler 模式有关,Class Table Inheritance

    如果您想使用参照完整性确保只有一个子表的行引用Log 中的给定行,则每个子表中的额外Log_Type 列是必要的。

    【讨论】:

    • 觉得给所有子表添加一个不必要的 Log_Type 是不雅的。另外,不确定这是否真的增强了我上面写的内容。
    【解决方案4】:

    我只需要一个包含受影响人员的日志表、一个 actionID 列和 item_id。

    然后在您的前端,您可以根据 actionID 显示通知,例如 actionID 1 可以是照片。所以你的 item_id 将是 photoID

    【讨论】:

    • 是的,所以这类似于我提到的“子”表......我只是觉得它非常不雅。外键不起作用,ORM 工具也不会总是轻易地处理这种情况。也许这是最好的解决方案,我只是希望有更优雅的东西......
    猜你喜欢
    • 1970-01-01
    • 2021-01-07
    • 2015-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-23
    相关资源
    最近更新 更多