首页 > 代码库 > 谈Apache OFbiz 会员模块表结构设计

谈Apache OFbiz 会员模块表结构设计

数据库表的结构设计可谓是ofbiz除技术框架之外,另一个非常值得学习的方向。这篇文章我们来谈谈ofbiz对电子商务会员表的设计。

PARTY

ofbiz对人、团体进行了抽象,称之为party,翻译为中文称之为“会员”(但我觉得抛开领域,如果你也有相关的设计需求,在其他领域可能称之为团体更合适)。会员在ofbiz被设计为一个抽象的概念(对应到面向对象设计中,你可以称其为一个基类),它有两个具体的延伸(继承者):分别是PERSON以及PARTY_GROUP。数据库的E-R图:


这里PERSON,PARTY_GROUP分别表示“个人会员”、“组织会员”。Party只是一种抽象,它定义了可以被抽象为“会员”的对象所具有的基本特征。但 “个人”以及“组织”会员却具备比 “基本会员”更多的特征,所以此处从Party延伸出两张表来存储这些额外的特征信息它们的主键都是PARTY表的PARTY_ID。

PARTY_TYPE

partyType定义了party的类型约束。E-R图如下:


可以看到,PARTY_TYPE是拥有层级关系的(它的一个属性PARENT_TYPE_ID自关联了PARTY_TYPE的主键:PARTY_TYPE_ID,下面如果看到E-R图上有自关联到本身的,都表示这种关系,不再敖述)。

ofbiz提供的初始数据中有如下几种party type:


构建成层级关系如下图所示:


上面展示的两张表:PERSON、PARTY_GROUP也是其中的两个partyType,并且这些partyType都可以独立扩展的,PERSON、PARTY_GROUP也是仅有的两个扩展。这也是上面表结构中这两个记录的HAS_TABLE值为Y的原因。

PARTY_ROLE

就跟社会的“角色分工”一样,一个会员在系统中也必定会拥有属于自己的角色。而PARTY_ROLE表就是用于关联会员与角色类型的关系表,很明显会员与角色类型是多对多的关系(这里需要提及的是:ofbiz中只有角色类型,没有角色,或者更准确点说,角色类型包含了角色)。


PARTY_RELATIONSHIP

上面我们看到的会员是一类“抽象”的实体。不管它表示的是个人,还是组织,它总是会跟其他会员发生关系,就好像一个人不可能脱离社会而孤立得存在着,他必然有自己的社会角色,并跟社会的其他“团体”产生联系。这在ofbiz中被抽象为“partyRelationship”。我们来看它是如果表达“关系”这个语义的:


当你把这些所有的字段连起来,它几乎能涵盖所有的“会员关系”(要知道,有时会员关系会非常复杂,一个会员有时会存在于多个系统中)。

我们再回过头来,看PARTY_RELATIONSHIP的表结构设计:


可以看到前五个键形成了联合主键,其中前四个都是形如XXX_FROM,XXX_TO的ID标识。表示从“FROM”方往“TO”方建立关系。其中PARTY_ID_FROM与ROLE_TYPE_ID_FROM是“源”方;PARTY_ID_TO与ROLE_TYPE_ID_TO是“目标”方。

从上面图中也可以看到,每个关系都带有两个DATETIME字段,分别表示:开始日期,截止日期。这说明关系是有“时段”这个属性的。当然,如果没有截止日期,可以看做是“永久”的。因此为了防止关系过了生效时段无法再次建立关系(因为主键不允许重复),所以选择了联合“FROM_DATE”作为联合主键(后面如果再次建立相同的关系时,只要FROM_DATE不一样,就视为一条新记录)。

这里有必要说明一下,在ofbiz的数据库设计中,大量采用了“时段”这个属性来标识记录的有效性。这样的设计与逻辑删除相比的好处是:它除了减少了删除时因为外键约束等连带关系导致的错误,还可以直接充当“历史记录”的作用,省去了对历史表的维护,当然它的缺点就是:表中的记录会比其他的设计多得多。

当然,From跟To只是为了标识两者建立了关系,却并未说明它们到底存在怎样的关系,就好像——我跟你是朋友。这句话可以拆分为三部分:FROM方:我,TO方:你,关系是:朋友。上表中用一个字段表示了关系:PARTY_RELATIONSHIP_TYPE_ID(这只是一个外键,关联着表PARTY_RELATIONSHIP_TYPE)。

在界面上新建一个关系(此处是从外部到自己的一个关系):


PARTY_RELATIONSHIP_TYPE

该表约束了关系的类型。比如:雇佣者、朋友、父、子、管理者,E-R图:


从图中可以看出,会员关系类型也拥有层次关系。表中还有两个特别的字段:

  • ROLE_TYPE_ID_VALID_FROM
  • ROLE_TYPE_ID_VALID_TO
它们用于约束这个关系的建立双方的角色。也就是说,不是任意的两个角色之间一定可以建立起某个特定的会员关系。当然这两个字段通常都为空,表示不对此加以限制。
对每个关系类型,都可以扩展以独立实现关系(被扩展后关系类型记录的字段HAS_TABLE被标识为Y,否则默认为N),在ofbiz的初始化数据中,唯一被扩展的关系类型是:EMPLOYMENT。我们来看看EMPLOYMENT关系表的实现:

可以看到,它跟之前的PARTY_RELATIONSHIP的主键实现方式一样。因此可以把它看做是:PARTY_RELATIONSHIP_TYPE_ID为EMPLOYMENT的PARTY_RELATIONSHIP的特殊实现。
在界面上建立一个关系类型:

PARTY_CLASSIFICATION_TYPE

为了便于管理,ofbiz对会员按各种维度进行分类,常见的分类的类型有:年收入、价值等级、产业、雇员数量等;

PARTY_CLASSIFICATION_GROUP

会员并不会直接跟分类的类型产生关系,而是跟一个或多个分类组产生关联关系。而分类组受分类类型约束。
新建一个分类组:

PARTY_CLASSIFICATION

会员的分类相关表的关系图:

从表的关联关系可以看出,会员分类跟分类组是多对多的关系,并且分类具有时效性。因此联合FROM_DATE作外键。
将会员划归入一个会员分类:

CONTENT_MECH

从这张表开始,我们来看会员的联系方式相关的表结构设计,这也是一部分非常棒的设计。

这张表存储了联系方式基本信息。它引用了另一张表:CONTENT_MECH_TYPE作为外键,来表示该联系方式的类型(通常的联系方式类型有电话、邮箱、网址等)。

CONTENT_MECH_TYPE


可以看到联系方式类型,也是具有层级结构(父子关系)的。
当我们想新建一个联系方式时,首先必须先指定想创建的联系方式的类型:

PARTY_CONTACT_MECH

毫无疑问,地址信息只有跟会员联系起来,才能表示会员的地址。而会员跟地址是多对多的关系,理解这个关系时需要注意的是会员可以是任何团体、组织或者个人。那这里可能就会存在两个不同的会员拥有同一个联系方式的可能,比如:一个员工会员与一个该员工所属的公司会员,它们可以都存在同一个联系方式:公司的通讯地址。当然一个会员拥有多个联系方式,这是很容易理解的。所以会员标识跟联系方式标识之间是多对多的关系,并且跟前面的设计模式相似——联系方式也有时效性,比如换电话号码,换工作导致联系方式变化等,所以联合FROM_DATE作为联合主键:

当我们选择联系方式类型为电话号码时,会出现如下的表单填写:

如果你新建一个联系方式的类型为电话号码,那么电话号码存储在何处?此处又跟前面谈到的HAS_TABLE字段有关(CONTACT_MECH_TYPE中也存在这个字段)。正常情况下,联系方式关联着联系方式类型,普通的联系方式的具体信息存储在CONTACT_MECH的INFO_STRING属性中。但有些联系信息不是单纯的像邮箱这样只是一个字符串,比如像电话号码、邮政编码…它们都有具体的格式表示。所以这些特例用INFO_STRING这一个属性存储也不方便,因此可以独立扩展该CONTACT_MECH_TYPE(将其HAS_TABLE字段设置为Y,这样查询该CONTACT_MECH信息的时候,就不采用INFO_STRING字段,而是采用扩展表中格式化的联系方式)。

CONTACT_MECH_PURPOSE_TYPE

当我们点击上面界面的保存按钮之后,会更进一步得扩充联系信息:

在ofbiz中还存在一个称之为“联系目的”的东西,它是什么意思?

看到选项我们就会明白,说白了一个人的地址簿或者电话簿中的联系方式可能有很多。它们没有主次之分,只有目的不同。

PARTY_CONTACT_MECH_PURPOSE

上面谈到了联系目的,那么很自然它需要跟会员具体的某条联系信息关联起来才能称之为:某个会员为了某种联系目的存储了一个“联系方式”记录。

这里需要注意的是,它并没有跟PARTY_CONTACT_MECH产生直接关联(没有外键关系),而是把PARTY_CONTACT_MECH的三个主键照搬过来,联合CONTACT_MECH_PURPOSE_TYPE_ID形成四个组合主键,这是因为PARTY_CONTACT_MECH的联合主键机制无法被其他表当做外键引用。因此,可以将PARTY_CONTACT_MECH_PURPOSE看作联系信息模块的聚合。这个怎么来理解?其实一个地址可以看成:某个会员(PARTY_ID),出于某种目的(CONTACT_MECH_PURPOSE_TYPE_ID),在某段时间内(FROM_DATE),保存了某个联系方式(CONTACT_MECH_ID)。这种联系方式的设计非常有弹性,因此在大部分情况下,这种抽象性能够涵盖大部分应用场景。

CONTENT_TYPE

会员内容的设计跟联系方式类似。会员可以有一个类似文件空间在服务器上,可以供其保存文档、图片之类的东西。CONTENT_TYPE限定了会员可以存储的内容类型:

CONTENT

该表是它的具体存储内容的地方,当然并不是唯一的,如果CONTENT_TYPE有一条记录的HAS_TABLE值为Y,则那个记录对应的表也用于存储内容。内容表里的字段非常多,就不截图了。
跟之前的联系信息类似,会员可以有多个内容,一个内容也可能从属于多个会员。因为会员是个抽象的概念,对应到实体上可能会有重合,所以需要一个“目的”来修饰会员内容,它就是——PARTY_CONTENT_TYPE。

PARTY_CONTENT_TYPE

用于修饰会员内容的用途,当然这里它的表名叫type,事实上从数据记录来看,来时充当了目的的作用。

内容还跟其他一些表有关联(主要是被引用关系,比如:CONTENT_ROLE等),此处因为跟本文主题没太大关系,所以不再敖述。

总结

更高的抽象级别

ofbiz party模块的设计,正如它所应用的场景:非常适用于电子商务系统会员信息相关的设计。当然ofbiz中其他相关的多个系统也同样应用了这些表结构,这也意味着它有适用于一般行业、系统的通用性,这得益于这种设计的抽象级别比较高。它可以描述任何的组织、个体、他们的地址信息、他们之间的关系。特别是对会员“relationship”表的设计非常类似于《分析模式》中谈到的责任模式:

因此,如果你面临组织结构比较复杂的业务场景时,比如群组、联系人、个人、公司都可以成为系统的用户,又或者一个非常大的跨国公司,拥有:总部、区域销售办公室、办事处、分公司等各种组织形式时,这种设计就会派上用场。

数据库表的继承关系

从PARTY、PARTY_TYPE、PARTY_GROUP、PERSON这几张表我们可以学习到数据库表的“继承”设计。

时效性设计

不是真删除、也不是逻辑删除、而是失效(FROM_DATE, THUR_DATE)。这种方式可以代替“操作-操作历史”的多表设计,转而合并为独立的一张表。

谈Apache OFbiz 会员模块表结构设计