前言
在写用户权限对应关系的时候突然想到一个问题:
一般我们设计用户权限表都是三个表,分别为用户表、权限表、用户权限关系表
(简单的用户权限对应,不考虑角色关系)
那么为什么必须要三个表而不能直接用用户表、用户权限关系表来实现呢?为什么非要权限表这个中间表呢?
要解决这个问题,首先要了解数据库设计里面的三大范式。
数据库三大范式
第一范式
第一范式(1NF)标明数据表中的每一列(字段)都应是不可再分的基本数据项,即保证每一列的原子性。
这个很好理解,比如我的用户表中有一个用户的家庭住址
字段,但是实际项目开发过程中我有个需求要求按用户的所在省或所在市对用户进行划分,那么显然只设计一个家庭住址字段是达不到需求的或者说是不合理的,其不满足数据项的原子性,正确设计应该将家庭住址
拆分为:省
、市
、详细地址
。
那么我是不是将字段分的越细就越好?这显然是对第一范式的扭曲理解。
举个很简单的例子,我用户表中有一个字段为文章分类
,目前一共有两个类型:心情窗和程序录,那么我为什么不用“心情窗、程序录”这两个字段而要使用“文章分类”呢?很明显,其一,它本身就不属于文章的属性,其二,文章只可能是两者之一的情况下,这样设计会产生大量的数据冗余。
这同样是不满足第一范式的。 虽然我举的例子可能不是特别的恰当,但是这种傻瓜类错误是很可能隐蔽的发生的,有些时候可能会项目进展到很后面才发现,对项目的修改和优化都是很大的麻烦,所以我们在设计数据库的时候就要尽量避免这一类的错误。
第二范式
第二范式(2NF)标明在满足1NF的前提下,非主键列必须完全依赖主键列,也就是说所有字段应描述的是一件事。
同样举个简单的例子,现在我有一个班级表
,其包括每个班级的基本信息与班主任信息,那么其中的班主任信息字段要如何设计?我需要班主任的电话,那么我是不是要设计两个字段:班主任编号、班主任电话?
很明显,将班主任的信息和班级信息糅在一个表里是极不合适的。
首先,班主任编号与电话与班级的其他字段如班级名称,这之中没有任何必然联系。
而且班主任电话依赖于班主任的编号,而班级名称依赖于班级编号,这已经是两件事情了。
其次,我们设想,如果一个人同时是多个班的班主任(只是举个例子…嘻嘻嘻),那么是不是会出现很多条重复数据?这会造成大量的数据冗余。这就是违反第二范式的典型例子。
所以在这种情况下,我们要保证班级表只记录班级的信息,班主任的信息则用另一张教师表来记录。
简单来说,就是你不能把两张表糅合为一张表,一张表中的字段只跟这张表的主键列相关。
第三范式
第三范式(3NF)标明在满足2NF的前提下,数据不存在传递关系,即表中的每一列(字段)都与主键直接相关而非间接相关。
继续用刚才的例子,在我已经分成了两张表:班级表、教师表 的情况下,我能不能在班级表
中只增加班主任的ID和班主任的电话这两个字段?
答案显然是不行的,如果你这么做了,就构成了一个间接关系:班级ID —> 教师ID —> 教师电话,这是不符合第三范式的。
所以我们应该在班级表中只记录教师ID,如果要查询教师信息,则根据这个ID在教师表进行详细查询。
总结
第一范式很好理解,就是保证数据项的原子性。一开始我总感觉第二范式和第三范式讲的是一件事情,其实不然。
第二范式主要在于有没有分成两张表,要求不能把不同的属性糅合在一张表内。
而第三范式是在已经分成两张表的前提下,一张表要和另一张表关联,则前者只能包含后者的主键ID而不能包含其他属性,否则会造成数据冗余。
那是不是遵循这三大范式的数据库设计就是最好的呢?
事实并非如此。除了数据库三大范式,还有反范式的存在,也就是不遵循数据库范式来设计数据库。
设计数据库时满足三大范式可以很大限度的合理的处理数据,减少数据的冗余,但是进行关联查询可能会拖慢查询速度,极大影响查询效率。
我认为,遵循范式亦或反范式要根据项目本身来进行决定,一个是时间换空间,一个是空间换时间,一切都应根据项目需求来进行合理的选择甚至混用。
比如数据量不是很大的情况下,并发量也不高,一个小应用,我就完全不必遵循第三范式而将数据字段糅合在一张表中,这样会很大程度上提高查询的效率。
(比如我将教师电话放在班级表中,那么当我查询班级表的时候就知道了班主任的电话,不必再借助主键进行关联查询了)
也就是说,如果一张数据表的数据变化很频繁的话,冗余数据会极大程度上的降低效率,这时就要遵循三大范式分为多张表设计,但是如果数据变化不是很频繁,增加冗余数据来提高查询效率也无不可。
总而言之,根据项目需求和表结构进行合理的选择设计,根据当时的情况来看是追求性能还是追求质量,适合自己的项目功能才是最好的设计。
(其实大部分情况下都是遵循范式的,反范式只有在特殊情况下才会用到,操作不灵活不说,操作的成本也会很高,后期维护管理起来也会很麻烦,所以最好还是遵循范式,分开处理)
问题解决
回到我们之前说的问题,为什么要用到权限表这个中间表?
其实如果不要中间表,是违反了第三范式的。
用户权限明显是一个多对多的关系,不用关联查询的话,如果一个用户拥有多个权限,就会产生极大的数据冗余。
所以关系对应表里面的应该是权限ID而非权限名称。
可能你会觉得权限表当前只有两个字段:权限ID与权限名称,这样做关联是否多此一举?但你有没有想过,如果以后你的权限表不止两个字段呢?如果只有两个表,是不是你每添加一个字段,这些就会多许多的重复数据?这个字段是跟权限直接相关而非跟用户直接相关的。
而你如果用权限表做中间表,那么关系表中我只用存ID的关系映射,添加字段往权限表中添加就好,这样做极大的方便了后期的维护。
而且还有一点,如果我现在要对权限做CURD,两个表怎么做?遍历吗?
这样看来,遵循范式给我们带来了极大的方便之处,即使是简单的用户权限设计,设计中间表也是很有必要的。
懒加载
这里再补充一点,就是Mybatis的懒加载机制。
懒加载是什么?顾名思义,懒加载就是延迟加载,就是我用到的时候才加载,不用到就不会加载。
在许多关联查询中,懒加载的加入可以使关联查询的效率得到极大改善。
比如用户权限信息,有很多时候我们只需要用户信息而不需要权限信息,我们就可以为用户查询设置懒加载,这样只有在登录时的查询才会进行关联查询来查询权限信息,而其他时候查询用户是不会执行关联查询的。
懒加载很好配置,因为我Mybatis用的纯注解配置,所以我这里只说注解版如何配置。
- 在
application.properties
中加入懒加载的配置。
# 查询时,关闭关联对象即时加载以提高性能
mybatis.configuration.lazy-loading-enabled=true
# 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能
mybatis.configuration.aggressive-lazy-loading=false
- 在
@One
/@Many
中加入懒加载的属性
/**
* 查询用户(连带权限信息)
* @param phone 用户手机号(用户名)
* @return 单个用户实体
*/
@Results(
id = "user", value = {
@Result(property = "role", column = "id", one = @One(fetchType= FetchType.LAZY, select = "com.seagull.myblog.mapper.RoleMapper.queryUserRole")),
@Result(property = "id", column = "id")
}
)
@Select("SELECT * FROM user WHERE phone LIKE #{phone}")
public User queryUserByPhone(String phone);
其中,fetchType= FetchType.LAZY
就是打开懒加载。
这样懒加载就全部配置完毕,当我们调用 queryUserByPhone
方法时,将不会执行关联查询(也就是 @One 后面跟的 select),而当我们获取 Role
时,才会对关联查询进行调用。
测试用例如下:
@Test
public void lazeLoad() {
User user = userMapper.queryUserByPhone("13900000000");
System.out.println(user.getId());
System.out.println(user.getRole());
}
根据打印sql语句的日志可以明显看到,当我们执行query
时,只调用了SELECT
查询用户表的一条查询语句,包括执行user.getId()
时,还是没有调用多余的语句。
直到执行user.getRole()
,才又调用了关联查询的SQL,将权限查询出来赋予用户。