反思以前对“多对多”关系处理的设计
反思以前对“多对多”关系处理的设计
很多公司的笔试题都喜欢考这一道:有n个供货商和m种货品,每个供货商可能供应多种货品,每种货品也有可能由多个供货商供应,建立相应的数据库表维护供应商和货品以及他们的供货关系。
这类多对多问题其实在真实的系统中很常见,比如用户--角色,角色--权限,用户--用户组等等。对接触过类似的系统的人这当然不是问题,三个表,套路来的嘛。
当然有其他方式,比如供货商表中添一个货品列表字段,用xml或者约定的格式保存货品列表,但是这样一是查询更新效率很低(查询的时候要解析数据,更新的时候要解析并重新组合数据,反向查找的时候更低效),二是没有办法建立外键约束。对第反向查找的问题,如果我们允许数据冗余并且有信心维护好数据一置性的话可以同时在货品表中添加一个供货商列表字段。但是除了效率、约束(以及冗余,假如它也算问题的话)之外,还有个最重要的问题我们忽略了:这样的设计不优雅。
但是在数据库表之外,我们做系统的时候是怎么处理类对应关系的呢?我没有看到过别人的设计,但是在我的系统中使用的其实是类似于上面说的第二种方式,比如在用户对象中有一个角色列表字段,存储了与用户关联的角色。当然解析数据带来的效率问题不存在了,因为角色信息是以原始方式保存的;数据一置性问题也由数据库解决。系统工作的很稳定也很快,但是我们一直忽略了这个问题:这样的设计优雅吗?我认为我们可以做的更好。
假如我们象设计数据库表一样的设计系统,系统本来可以做成这样子的:
(Database)
tb_user tb_user_role tb_role
------------------------------------------------------------------------------------
UserDAO UserRoleDAO RoleDAO
UserMng UserRoleMng RoleMng
这样user的model对象里面就不需要维护role列表了。我们需要访问关联关系的时候可以通过UserRoleMng来获得列表,如果我们需要访问role对应的user列表也一样的简单(在原来的设计中要遍历全部user)。
更进一步,我们把对这种多对多关联关系的处理抽取出来,做一个基类:
abstract class AbstractRelationManager{
protected void init();
protected void addRelation(id1,id2){
...
}
protected void deleteRelation(id1,id2)
...
}
protected List getId1ListById2(id2){
...
}
protected List getId2ListById1(id1){
...
}
}
然后UserRoleMng、RolePermissionMng或者其他类似的关系管理器都可以继承自AbstractRelationManager并由超类处理关联查询的逻辑:
class UserRoleMng{
public void addRelation(userId,roleId){
super.addRelation(userId,roleId);
}
...
}
如果高兴还可以这样包装
class UserRoleMng{
public void addRelation(user,role){
super.addRelation(user.getId,role.getId);
}
...
}
这样的设计看起来比原来的要好一点了,但是有一个问题是,按我的经验AbstractRelationManager很可能需要使用模版方法模式(templet method)来实现,这样不可避免的会违反依赖倒易原则(DIP)并降低代码的可读性,与我们的初衷有些背离。当然templet method也可以用代理类来代替,但是这样的实现在我看来不但复杂而且更不优雅。另一种代替的方式是不采用templet method而把对模版方法的调用实现在每个子类中,这样要求每个子类严格的符合编码约定,而且带来了拷贝代码的臭味。相比之下,我还是宁可选择在必要的时候templet method。
最后,我们有可能需要处理更复杂的关系,比如:
group------group-------user
| |---------user
| group-------group------user
| | |---------user
| |---------user
|---------user
user-group关系的一方,group是具有递归的层次结构的对象。这中情况下我们大概还需要从AbstractRelationManager继承一个新的抽象类
abstract AbstractRecurveRelationManager AbstractRelationManager{
protected List getId1ListById2Recurve(id2){
//递归方法id2对应的对象的子树并获得所有树节点的id1列表。
}
}