在Mybatis
中,有一个强大的功能元素resultMap
。当我们希望将JDBC ResultSets
中的数据,转化为合理的Java对象时,你就能感受到它的非凡之处。正如其官方所述的那样:
resultMap
元素是 MyBatis
中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets
数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap
能够代替实现同等功能的长达数千行的代码。ResultMap
的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。
在Mybatis
中,最简单的结果映射方式,就是通过类型别名typeAliases
来处理。
如果要这样做,那么第一步需要配置实体类包的路径:
mybatis.type-aliases-package=com.xxx.entity
该路径下的所有类,就会被注册到TYPE_ALIASES容器
。我们在指定返回值类型的时候,就直接用别名即可。
比如,我们有一个User
类:
1 2 3 4 5 6 7 8 |
@Data public class User { private String id; private String username; private String password; private String address; private String email; } |
如果数据库中表的字段与User
类的属性名称一致,我们就可以使用resultType
来返回。
1 2 3 4 5 6 7 8 9 10 |
<select id="getUsers" resultType="User"> SELECT u.id, u.username, u.password, u.address, u.email FROM USER u </select> |
当然,这是理想状态下,属性和字段名都完全一致的情况。但事实上,不一致的情况是有的,这时候我们的resultMap
就要登场了。
如果User
类保持不变,但SQL
语句发生了变化,将id
改成了uid
。
1 2 3 4 5 6 7 8 9 10 |
<select id="getUsers" resultType="User"> SELECT u.id as uid, u.username, u.password, u.address, u.email FROM USER u </select> |
那么,在结果集中,我们将会丢失id
数据。这时候我们就可以定义一个resultMap
,来映射不一样的字段。
1 2 3 |
<resultMap id="getUserByIdMap" type="User"> <result property="id" column="uid"></result> </resultMap> |
然后,我们把上面的select
语句中的resultType
修改为resultMap="getUserByIdMap"
。
这里面column
对应的是数据库的列名或别名;property
对应的是结果集的字段或属性。
这就是resultMap
最简单,也最基础的用法:字段映射。
下面,我们看看其他几种标签都是怎么应用的。
元素名称 | 描述 |
---|---|
constructor | 用于在实例化类时,注入结果到构造方法中 |
association | 关联一个对象 |
collection | 关联多个对象 |
如果你希望将结果注入构造方法里,就可以用到constructor
元素。
比如,我们的User
类增加了一个构造方法:
1 2 3 4 |
public User(String id, String name) { this.id = id+"--------"; this.username = name+"--------"; } |
我们需要在resultMap
中定义constructor
元素:
1 2 3 4 5 6 |
<resultMap id="getUserByIdMap" type="User"> <constructor> <idArg column="id" name="id" javaType="string"></idArg> <arg column="username" name="name" javaType="string"></arg> </constructor> </resultMap> |
其中,column
代表数据库字段名称或者别名;name
则是构造方法中的参数名称;javaType
指定了参数的类型。
如你所想,这样指定构造方法后,我们结果集中的id和username
属性都会发生变化。
1 2 3 4 5 6 7 |
{ "id": "1001--------", "username": "后羿--------", "password": "123456", "address": "北京市海淀区", "email": "510273027@qq.com" } |
在实际的业务中,我们的用户一般都会有一个角色。那么在User
类里面一般也是以一个实体类来表示。
1 2 3 4 5 6 7 |
@Data public class User { //省略用户属性... //角色信息 private Role role; } |
我们在查询用户的时候,如果也希望看到它的角色信息,我们会这样来写查询语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<select id="getUserById" resultType="User"> SELECT u.id, u.username, u.password, u.address, u.email, r.id as 'role_id', r.name as 'role_name' FROM USER u LEFT JOIN user_roles ur ON u.id = ur.user_id LEFT JOIN role r ON r.id = ur.role_id where u.id=#{id} </select> |
如上,就要查询单个用户以及用户的角色信息。不过在这里,我们不能用resultType=User
来返回。
毕竟,User
类中只有一个Role
对象,并没有role_id和role_name
字段属性。
所以,我们要使用association
来关联它们。
1 2 3 4 5 6 7 8 9 10 11 12 |
<resultMap id="userMap" type="User"> <id property="id" column="id"></id> <result property="username" column="username"></result> <result property="password" column="password"></result> <result property="address" column="address"></result> <result property="email" column="email"></result> <association property="role" javaType="Role"> <id property="id" column="role_id"></id> <result property="name" column="role_name"></result> </association> </resultMap> |
最后我们就可以将角色信息一块显示出来:
1 2 3 4 5 6 7 8 9 10 11 |
{ "id": "1001", "username": "后羿", "password": "123456", "address": "北京市海淀区", "email": "3553154544@qq.com", "role": { "id": "3", "name": "射手" } } |
事实上,如果你确定关联信息是一对一
的情况,有个更简便的方法可以替代association
,我们在本文的第五部分-自动填充关联对象
再看它是怎么实现的。
上面我们看到一个用户后羿
,它的角色是射手
;但大部分时候,我们每个人都不可能只拥有一种角色。所以,我们需要将User
类中的角色属性的类型改成List
。
1 2 3 4 5 6 7 |
@Data public class User { //省略用户属性... //角色信息 private List<Role> roles; } |
现在就变成了一个用户对应多个角色,所以就不是简单的association
。
因为association
处理的是有一个
类型的关联;而我们这里是有多个
类型的关联,所以就需要用到collection
属性。
我们整体的resultMap
会变成下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 |
<resultMap id="userMap" type="User"> <id property="id" column="id"></id> <result property="username" column="username"></result> <result property="password" column="password"></result> <result property="address" column="address"></result> <result property="email" column="email"></result> <collection property="roles" ofType="Role"> <id property="id" column="role_id"></id> <result property="name" column="role_name"></result> </collection> </resultMap> |
这样的话,即便你有多个角色也可以被正确显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "id": "1003", "username": "貂蝉", "password": "123456", "address": "北京市东城区", "email": "510273027@qq.com", "roles": [ { "id": "1", "name": "中单" }, { "id": "2", "name": "打野" } ] } |
在大部分业务系统中,我们都会有一个菜单的表,比如像下面这样,一张Menu
表:
id | name | url | parent_id |
---|---|---|---|
1 | 系统管理 | 0 | |
1001 | 用户管理 | /user | 1 |
1002 | 角色管理 | /role | 1 |
1003 | 单位管理 | /employer | 1 |
2 | 平台监控 | 0 | |
2001 | 系统监控 | /system/monitor | 2 |
2002 | 数据监控 | /data/monitor | 2 |
这里我们给菜单分为两级。我们给前端返回菜单的时候,也是需要分级的,不可能将这7条数据平级展示。那么,在这里我们的Menu
实体类如下:
1 2 3 4 5 6 7 8 |
@Data public class Menu { private String id; private String name; private String url; private String parent_id; private List<Menu> childMenu; } |
一级菜单,包含一个二级菜单的列表,这里用childMenu
来表示。
SQL
语句中,如果没有parent_id
字段属性,我们就先查所有的一级菜单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<select id="getMenus" resultMap="menusMap"> SELECT m.id, m.name, m.url, m.parent_id FROM m_menu m where 1=1 <choose> <when test="parent_id!=null"> and m.parent_id = #{parent_id} </when> <otherwise> and m.parent_id = '0' </otherwise> </choose> </select> |
这个查询语句,在不传输任何参数的情况下,我们会得到两条一级菜单的数据。
那么在只调用此方法一次的情况下,怎么把所有的菜单信息查询出来,并按层级展示呢?
我们来看menusMap
的定义:
1 2 3 4 5 6 7 8 9 |
<resultMap id="menusMap" type="Menu"> <id property="id" column="id"></id> <result property="name" column="name"></result> <result property="url" column="url"></result> <result property="m_desc" column="m_desc"></result> <result property="parent_id" column="parent_id"></result> <collection property="childMenu" ofType="Menu" select="getMenus" column="{parent_id=id}"></collection> </resultMap> |
重点来看collection
元素:
property="childMenu"
对应的是菜单中的子级菜单列表;
ofType="Menu"
对应返回数据的类型;
select="getMenus"
指定了SELECT
语句的id;
column="{parent_id=id}"
则是参数的表达式。
这个collection
整体的含义可以这样理解:
通过getMenus
这个SELECT语句
来获取一级菜单中的childMenu
属性结果;在上面的SELECT语句
中,需要传递一个parent_id
参数;这个参数的值就是一级菜单中的id
。
通过这种方式,我们就可以得到已分级的所有菜单信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
[ { "id": "1", "name": "系统管理", "parent_id": "0", "childMenu": [ { "id": "1001", "name": "用户管理", "url": "/user", "parent_id": "1" }, { "id": "1002", "name": "角色管理", "url": "/role", "parent_id": "1" }, { "id": "1003", "name": "单位管理", "url": "/employer", "parent_id": "1" } ] }, { "id": "2", "name": "平台监控", "parent_id": "0", "childMenu": [ { "id": "2001", "name": "系统监控", "url": "/system/monitor", "parent_id": "2" }, { "id": "2002", "name": "数据监控", "url": "/data/monitor", "parent_id": "2" } ] } ] |
我们知道,在Mybatis
解析返回值的时候。
第一步是获取返回值类型,拿到Class
对象,然后获取构造器,设置可访问并返回实例,然后又把它包装成MetaObject
对象。
从数据库rs
中拿到结果之后,会调用MetaObject.setValue(String name, Object value)
来填充对象。
在这过程中,有趣的是,它会以.
来分隔这个name
属性。
如果name
属性中包含.符号
,就找到.符号
之前的属性名称,把它当做一个实体对象来处理。
可能笔者在这里描述的不够直观,我们还是来看例子。
在本文第三部分
中,我们有一个用户对应一个角色
的例子。
其中,User
类定义如下:
1 2 3 4 5 6 7 |
@Data public class User { //省略用户属性... //角色信息 private Role role; } |
在这里,我们无需定义resultMap
,直接返回resultType=User
即可。不过需要把role
信息的别名修改一下,重点是.符号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<select id="getUserList" resultType="User"> SELECT u.id, u.username, u.password, u.address, u.email, r.id as 'role.id', r.name as 'role.name' FROM USER u LEFT JOIN user_roles ur ON u.id = ur.user_id LEFT JOIN role r ON r.id = ur.role_id </select> |
这样,在Mybatis
解析到role.id
属性的时候,以.符号
分隔之后发现,role
别名对应的是Role
对象,则会先初始化Role
对象,并将值赋予id
属性。
相关代码如图:
来源:掘金
作者:清幽之地
from:https://zhuanlan.zhihu.com/p/79153088