用于记录用户操作的优雅模式

时间:2011-06-22 22:39:32

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

我有一个数据库模式来记录用户在我的webapp中执行的操作:

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表必须包含每种可能的用户操作类型(他们修改了故事或评论,创建了故事或评论或照片,更新了配置文件等)。对于每个条目,几乎所有这些字段都必须为空。

理想情况下,我有整个日志所指的各个日志表,可能是这样的:

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中存储子日志表的名称,但这看起来很不优雅。是否有一种更好,更具关系性的方法,这种方式也适用于ORM系统?

4 个答案:

答案 0 :(得分:2)

这对于引入通用数据模型或者至少是数据模型中的泛型类型系统是一个很好的观点。这个概念是一切都有条目,甚至是行动,人,页面,流程等等。当它到位时,您需要一种在这些实体之间创建任意关系的通用方法,使它们之间的链接相当容易。您的问题是为什么我推广更通用的数据模型而不是我们通常使用的超常规数据模型的例子之一。

我使用最多的模型是Topic Maps(即使这些信息可能不是最容易理解我正在谈论的内容),而不是每个实体都有一个表,有一个掌握所有,以及一些额外的处理它们之间的典型和关系。您不必一直使用它,但也许可以专门用于您的用例。这是近10年前的an article I wrote about it,而another one by Marc de Graauw也是关于它的特定RDBMS视图。

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

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

这将为您提供基础知识(但是如果您想要完整的monty,可以扩展和实现大量内容,例如支持多种类型,持久性标识,本体分组,以及其中也是主题地图的一部分),并给你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

第一个成员是比利鲍勃,扮演'人'的角色。下一步;

id: {whatever}
topic_ref: 34598067
role_topic_ref: 56987

此处,“添加新用户”主题扮演“行动”的角色。您可以使用您认为需要的多个项目来扩展此关联,例如添加前置状态,操作结果,到目前为止的尝试次数,操作发生的位置(例如,如果它是人们可以执行的功能)许多页面),以及不断。在Topic表中为这些东西创建实体,为它们的关系创建实体,并且可以根据需要使它变得复杂。

这一切看起来有点刺耳,但它非常灵活,您根本不需要更改数据模型以用于将来的扩展。我已经用这个模型构建了多年的系统,我只有赞美它。如果您想沿着该路径前进,则主题属性的单独表将遵循关联成员的模型。

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

答案 1 :(得分:2)

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

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

因此,如果案件是车辆,卡车和轿车,每辆卡车或轿车都会在车辆工作台上有一个入口,卡车也会在卡车桌上有一个入口,其PK值是相应PK的副本。车辆表。同样适用于轿车。只需进行连接就可以很容易地判断出车辆是卡车还是轿车,而且您通常也想在这种查询中加入数据。

答案 2 :(得分:1)

我推荐你描述的第二个设计。如果要获取每个日志子类型表的所有列,可以使用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_Type中的给定行,则每个子表中的额外Log列是必需的。

答案 3 :(得分:0)

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

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