本章介绍一些高级主题,处理关联映射,鉴别器,存储过程,类型处理器。其中存储过程在实际项目中很少遇到,所以省略这部分的内容。
具体内容如下:
- 第一小节介绍如何配置映射关系,一对一,一对多,多对多的情形以及鉴别器的用法。
- 第二小节介绍如何处理存储过程,这部分的内容忽略。
- 第三小节介绍如何处理常见的数据类型,如何编写自定义的类型处理器。
1、结构图
2、关联映射
关联关系有三种1对1,1对多,多对多,多对多可以拆分为两个单向的一对多。在配置时只存在1对1和1对多的配置。
映射这里指将数据库查询的结果集映射为Java实体类的属性,默认情况下会根据驼峰方式自动映射,例如user_name会映射为userName,id会映射为id。
关联映射,理解为当实体之间的关系为1对1时,结果集如何转换为Java实体类的属性,当为多对多事,又如何转换。
下面具体介绍
2.1 1对1
假定用户与它的家庭地址是一对一的关系,例如User类中存在homeAddress,它的家庭地址又包含五个字段,国家,省份,城市,街道,详细地址(门牌号等)。
当关联关系为1对1时,映射方式有两种:
- 自动转换,默认情况下是驼峰方式,例如user_name映射为userName,而homeAddress.country会映射为HomeAddress类的country属性。这种映射关系与前端表单映射到后端参数的方式是一样的。此时无需任何配置
- 在resultMap中配置association标签,它也有三种方式
- 在内部重复罗列HomeAddress类中的所有属性,因为肯定在HomeAddressMapper配置文件中存在resultMap。
- 使用association标签的resultMap属性,复用HomeAddressMapper中的配置,格式为namespace.resultMapID,其中namespace表示HomeAddressMapper对应的命名空间,resultMapID表示resultMap的id属性值。
- 使用association标签的select属性,它的值为select标签的ID属性,该select的返回值必须是HomeAddress对象。
如下是association标签的属性:
表格6- 1 association标签
描述 |
配置对象为一对一关联关系时,数据库结果集与Java实体类之间的映射关系 |
|
属性 |
property |
描述: 指定关联对象的属性名称 |
示例:上述user与HomeAddress的例子,此值应该为homeAddress |
||
属性 |
JavaType |
描述:指定关联对象的类型。 |
示例:此值应该为HomeAddress类的全名 |
||
属性 |
resultMap |
描述:指定关联对象的映射关系 |
示例:此值应该为HomeAddressMapper.resultMapID,不用重复罗列HomeAddress中的属性值 |
||
属性 |
columnPrefix |
描述:指定结果集对应关联对象的列前缀。 |
示例:为了区分关联查询时地址表的列,给其添加homeAddress前缀。XX as homeAddress.country。 |
||
属性 |
Select |
描述:指定select查询语句,它的返回值必须是关联对象 |
示例:此值应该为HomeAddressMapper中的selectAdressByUserId。类似这种查询 |
||
属性 |
column |
描述:建立select语句查询结果集与resultMap中列配置的映射关系。例如HomeAddressMapper中的select语句它会返回country列,它需要对应homeAddress.country,然后在转换为HomeAddress中的country属性。 |
示例:HomeAddressMapper中select语句的结果集如果不与User中的属性重名,可以使用默认驼峰方式。 |
||
属性 |
fetchType |
描述:加载方式,延迟加载还是立刻加载。值为lazy和eager。理解该属性首先需要理解N+1的问题,假设执行selectUser查询用户,它会查询所有满足条件的用户,每个用户都关联一个HomeAddress,当指定associate的select属性时,每个用户都对应一个HomeAddress,所以每个用户的HomeAddress都需要一次查询,所以N个用户会导致查询HomeAddress表N次。再加上查询用户,总共执行了N+1次查询,其中N次查询HomeAddress,1次查询用户。假设用户关联的不止HomeAddress一个类,那可想而知,会执行mN +1次查询,m为与User一对一关系的对象数量。 指定为懒加载方式时,只有使用到该用户时,才会去查询HomeAddress表。即使这样它的效率还是不如关联查询快,所以慎用select方式。 |
示例:此值应该为lazy。 |
||
链接 |
https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#association |
2.2 1对多
1对多与1对1的映射方式类似,标签只是从association变为collection。区别在于如何判断1对多的关系。这里以用户User和角色Role为例,从数据库表的角度看
当数据库表之前存在主外键关系时,即Role表中存在user_id字段。
在映射的过程中道理也是一样的,当用户与角色进行关联查询时,角色的user_id列与用户ID列值相同时,这些角色都归属与该用户。
在配置Collection标签时也必须体现主外键的关系,User的resultMap和Collection标签内部的Role属性都必须配置ID属性。
例如UserMapper中的
<resultMap> <id property="id" column="id" /> <collection property=”roles” javaType=”list” ofType=”Role”> <id property="id" column="user_id"/> </collection> </resultMap>
此时结果集中具有相同user_id的会合并,并映射为一个User对象中。若不配置此项,会导致每一条结果集映射为一个User对象,除非结果集中所有列的数据都相同,才会映射为相同User对象,这种情况一般不存在,因为ID肯定是不同的。
2.3 多对多
在理解1对多之后,多对多的关系非常简单。它可以拆分为两个1对多的关系,例如用户和角色,需要在UserMapper中配置collection标签,同时在RoleMapper中配置collection标签。
3、鉴别器
鉴别器的作用相当于在映射过程中添加分支,可以理解为当结果集中某一列的值为XX时,指定XX对应的resultMap。语法相当于java的switch语句,外层的resultMap相当于default分支。
下述例子中,当结果集中user_name的属性值为admin时,映射关系为ResultMapWithBLOBS。
当结果集中的user_name为其他值时,映射关系为userDiscriminatorMap。
<!-- user discriminator 这段配置的含义是当用户名为admin,加载BLOB的字段,否则加载基本的字段 --> <resultMap id="userDiscriminatorMap" type="com.module.mybatis.bean.User" extends="BaseResultMap"> <discriminator javaType="string" column="user_name"> <case value="admin" resultMap="ResultMapWithBLOBs"/> </discriminator> </resultMap>
可以尝试练习原书中所给的示例。
4、类型处理器
类型处理器的作用是将结果集中的数据类型转换为Java实体中属性的数据类型,这是全局的,不用为每一个属性单独配置。
自定义类型处理器的步骤如下:
- 编写自定义类型处理器,继承自BaseTypeHandler或者实现TypeHandler接口
- 注册自定义类型处理器,在mybatis-config中配置typeHandler标签。
示例如下:这个例子是将地址字符串转换为Address类,地址字符串的格式为country_province_city_street_otherDetails(国家_地区_城市_街道_其他详细信息)
// import语句省略,Address类有以上五个属性,都是字符串类型 public class AddressHandler extends BaseTypeHandler<Address> { @Override public Address getNullableResult(ResultSet rs, String columnName) throws SQLException { Address address_obj = new Address(); // 根据列名称获取 String address_string = rs.getString(columnName); // 获取column列中的值,映射成Address类 if (null == address_string) { return null; } else { String[] address = address_string.split("_"); address_obj = new Address(address); } return address_obj; } @Override public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException { Address address_obj = new Address(); // 根据列的索引获取 String address_string = rs.getString(columnIndex); // 获取column列中的值,映射成Address类 if (null == address_string) { return null; } else { String[] address = address_string.split("_"); address_obj = new Address(address); } return address_obj; } @Override public Address getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { // 一般不会使用到这个方法,因为STATEMENT="PREPARED",就是JDBC中的PreparedStatement对象 Address address_obj = new Address(); String address_string = cs.getString(columnIndex); // 根据列的索引获取 if (null == address_string) { return null; } else { String[] address = address_string.split("_"); address_obj = new Address(address); } return address_obj; } @Override public void setNonNullParameter(PreparedStatement ps, int i, Address paramAddress, JdbcType jdbcType) throws SQLException { ps.setString(i, paramAddress.toString()); } }
注册配置如下:
<typeHandlers> <!-- 日期的类型处理器 --> <typeHandler javaType="java.util.Date" handler="org.apache.ibatis.type.DateTypeHandler"></typeHandler> <typeHandler javaType="com.xx.bean.Address" handler="com.xx.handler.AddressHandler"></typeHandler> </typeHandlers>
类型映射在日常编程很容易见到,例如前端往后端传数据时,将JSON字符串转换为实体对象,将表单的字符串转换为对象等等。本质上是类似的,只不过这个转换过程是自动进行的,spring框架也允许自定义类型转换,例如注册自定义PropertyEditor。