文章目录
- 1.一对一映射
- 2.一对多映射
- 3.多对多映射
- 4.自定义类型映射
- 4.1 枚举类型案例
- 4.2 货币类型案例
- 5.分页插件
当使用 MyBatis 进行对象关系映射(ORM)时,我们经常需要处理一对一映射、一对多映射和多对多映射的关系。同时还可能遇到需要进行自定义类型映射和分页查询的场景。
注意:为了节约篇幅,这里直接给出处理方案,不再进行完整程序的演示。
1.一对一映射
一对一映射指的是两个实体之间的关系,其中一个实体与另一个实体关联,每个实体实例只能关联一个对应的实体实例。假设有两个实体类 User
和 Account
,每个 User
有一个 Account
,即一对一的关系。
如果是基于 XML 映射文件的方式,我们可以通过 ResultMap
+ association
标签来实现:
<!-- UserMapper.xml -->
<select id="getUser" resultMap="userAccountMap">SELECT u.*, a.* FROM user u LEFT JOIN account a ON u.id = a.user_id WHERE u.id = #{id}
</select><resultMap id="userAccountMap" type="User"><id property="id" column="id"/><result property="username" column="username"/><result property="password" column="password"/><association property="account" javaType="Account"><id property="id" column="account_id"/><result property="userId" column="user_id"/></association>
</resultMap>
这里的 <association>
标签就是一对一的映射关系,通过 javaType
属性指定类型,将查询结果映射到 User
的 account
属性上。
如果是基于注解则对应如下:
// UserMapper.java
@Select("SELECT u.*, a.* FROM user u LEFT JOIN account a ON u.id = a.user_id WHERE u.id = #{id}")
@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "username", column = "username"),@Result(property = "password", column = "password"),@Result(property = "account", column = "account_id", javaType = Account.class,one = @One(select = "cn.javgo.mapper.AccountMapper.selectAccount"))
})
User selectUser(Integer id);
这里的 @One
注解表示一对一的映射关系,可以看到 @One
中给定的是具体获取方法的全限定类名加方法名,会根据 User
的 account_id
字段作为参数查询对应的 Account
。
2.一对多映射
一对多映射指的是两个实体之间的关系,其中一个实体与多个另一个实体关联,而多个另一个实体实例只能关联一个实体实例。假设有两个实体类 Class
和 Student
,一个 Class
有多个 Student
,即一对多的关系。
如果是基于 XML 映射文件的方式,处理一对多关系,我们可以使用 ResultMap
+ collection
标签来实现:
<!-- ClassMapper.xml -->
<select id="getClass" resultMap="classStudentMap">SELECT c.*, s.* FROM class c LEFT JOIN student s ON c.id = s.class_id WHERE c.id = #{id}
</select><resultMap id="classStudentMap" type="Class"><id property="id" column="id"/><result property="name" column="name"/><collection property="students" ofType="Student"><id property="id" column="student_id"/><result property="name" column="student_name"/></collection>
</resultMap>
这里的 <collection>
标签就是一对多的映射关系,将查询结果映射到 Class
的 students
属性上。
如果是基于注解则对应如下:
// ClassMapper.java
@Select("SELECT c.*, s.* FROM class c LEFT JOIN student s ON c.id = s.class_id WHERE c.id = #{id}")
@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),@Result(property = "students", column = "id", javaType = List.class,many = @Many(select = "cn.javgo.StudentMapper.selectStudentsByClassId"))
})
Class selectClass(Integer id);
这里的 @Many
注解表示一对多的映射关系,可以看到 @Many
中给定的是具体获取方法的全限定类名加方法名,会根据 Class
的 id
字段作为参数查询对应的所有 Student
。
3.多对多映射
多对多映射指的是两个实体之间的关系,其中一个实体与多个另一个实体关联,多个另一个实体实例也可以关联多个实体实例。例如,学生表(Student
)和课程表(Course
),一个学生可以选修多门课程,一门课程也可以被多个学生选修。这其实与上述的一对多关系本质上是一样的,因此处理方案也相同。
以下是基于 XML 的示例:
<select id="getStudent" resultMap="studentCourseResult">SELECT * FROM Student s LEFT JOIN StudentCourseRelation scr ON s.id = scr.student_id LEFT JOIN Course c ON scr.course_id = c.id WHERE s.id = #{id}
</select><resultMap id="studentCourseResult" type="Student"><id property="id" column="id" /><result property="name" column="name" /><collection property="courses" ofType="Course"><id property="id" column="course_id" /><result property="courseName" column="course_name" /></collection>
</resultMap>
以下是基于注解的示例:
public interface StudentMapper {@Select("SELECT * FROM Student s LEFT JOIN StudentCourseRelation scr ON s.id = scr.student_id LEFT JOIN Course c ON scr.course_id = c.id WHERE s.id = #{id}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),@Result(property = "courses", column = "student_id", javaType = List.class,many = @Many(select = "cn.javgo.mapper.CourseMapper.selectByStudentId"))})Student getStudent(Long id);
}
4.自定义类型映射
MyBatis 允许你在几乎任何时候都使用自定义的 TypeHandler
来处理 SQL 语句的参数绑定以及结果映射。如果你有一个特定的数据类型需要做一些特殊的处理,你可以编写自定义的 TypeHandler
。
4.1 枚举类型案例
首先,需要实现 TypeHandler
或 BaseTypeHandler
抽象类。例如,有一个枚举类型 State
,包含了 ACTIVE
和 INACTIVE
两个状态:
/*** 状态枚举*/
public enum State {ACTIVE(1, "激活"),INACTIVE(0, "未激活");private Integer code;private String desc;State(Integer code, String desc) {this.code = code;this.desc = desc;}public Integer getCode() {return code;}public String getDesc() {return desc;}public static State getByCode(Integer code) {for (State state : State.values()) {if (state.getCode().equals(code)) {return state;}}return null;}
}
但在数据库中,我们希望它们分别保存为 1 和 0,可以定义一个自定义的类型处理器重写该类的四个方法:
/*** 自定义枚举类型转换器({@link State} -> {@link Integer})*/
public class StateTypeHandler extends BaseTypeHandler<State> {/*** 用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型( Java -> DB )* @param preparedStatement 用于设置参数的PreparedStatement对象* @param i 参数的位置* @param state 参数的值* @param jdbcType JDBC类型* @throws SQLException 数据库异常*/@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, State state, JdbcType jdbcType) throws SQLException {preparedStatement.setInt(i, state.getCode());}/*** 用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param s 字段名称* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(ResultSet resultSet, String s) throws SQLException {int code = resultSet.getInt(s);return State.getByCode(code);}/*** 用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(ResultSet resultSet, int i) throws SQLException {int code = resultSet.getInt(i);return State.getByCode(code);}/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param callableStatement CallableStatement对象* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(CallableStatement callableStatement, int i) throws SQLException {int code = callableStatement.getInt(i);return State.getByCode(code);}
}
然后,在 Spring Boot 的配置文件中配置 TypeHandler
所在的包路径:
mybatis:type-handlers-package: cn.javgo.learningmybatis.support.handler
最后,在 mapper XML 文件中,你就可以直接使用 State
类型了:
<select id="selectByState" parameterType="cn.javgo.learningmybatis.enums.State" resultType="User">SELECT * FROM User WHERE state = #{state}
</select>
在注解方式中,你可以直接在 @Results
注解中使用 @Result
注解的 typeHandler
属性来指定 TypeHandler
:
public interface UserMapper {@Select("SELECT * FROM User WHERE state = #{state}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "username", column = "username"),@Result(property = "state", column = "state", javaType = State.class, typeHandler = StateTypeHandler.class)})List<User> selectByState(@Param("state") State state);
}
这样,当查询 User
并将结果映射到 User
对象时,state
字段将使用我们自定义的 StateTypeHandler
来处理。
TIP:
MyBatis 为 Java 枚举类型的处理提供了两种方式:
EnumTypeHandler
和EnumOrdinalTypeHandler
。
EnumTypeHandler
:默认枚举处理器,它将枚举的名称(如 ACTIVE 或 INACTIVE)保存到数据库中。EnumOrdinalTypeHandler
:它将枚举的顺序(ordinal)保存到数据库中,如 1 或 0。因此,我们其实可以省略上述的操作,直接使用现成的这两个处理器就可以实现枚举相关的操作了。
4.2 货币类型案例
在实际场景中,我们只有在 MyBatis 没有提供合适的内置 TypeHandler
时,才自定义自己的类型处理器。一个常见的例子就是讲 Money
类型的属性值和 Long
类型之间的转换,因为金额一般我们都是用 Money
类来表示,但是在数据库中一般却以分为单位进行存储,在取出时则以人民币为币种还原为 Money
。
这里需要使用一个货币相关的类库,Joda-Money 是一个开源的 Java 库,旨在提供强大而灵活的处理货币和货币金额的功能。它是 Joda-Time 日期和时间库的姊妹项目,专门用于处理货币价值和货币操作。Joda-Money 提供了一组类和方法,使您能够进行精确的货币计算和处理。
需要使用该类库需要添加如下依赖:
<!-- Joda Money -->
<dependency><groupId>org.joda</groupId><artifactId>joda-money</artifactId><version>1.0.1</version> <!--请根据实际情况选择版本号-->
</dependency>
处理 Money
类型的 MoneyTypeHandler
代码如下:
/*** 自定义类型转换器({@link Money} -> {@link Long})*/
public class MoneyTypeHandler extends BaseTypeHandler<Money> {/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param value 数据库中的数据* @return 转换后的Java对象*/private Money parseMoney(Long value) {// 创建Money对象(货币单位为分,货币类型为人民币)return Money.ofMinor(CurrencyUnit.of("CNY"), value);}/*** 用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型( Java -> DB )* @param preparedStatement 用于设置参数的PreparedStatement对象* @param i 参数的位置* @param money 参数的值* @param jdbcType JDBC类型* @throws SQLException 数据库异常*/@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, Money money, JdbcType jdbcType) throws SQLException {// 获取金额(分),并设置到PreparedStatement对象中preparedStatement.setLong(i, money.getAmountMinorLong());}/*** 用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param s 字段名称* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(ResultSet resultSet, String s) throws SQLException {return parseMoney(resultSet.getLong(s));}/*** 用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(ResultSet resultSet, int i) throws SQLException {return parseMoney(resultSet.getLong(i));}/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param callableStatement CallableStatement对象* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(CallableStatement callableStatement, int i) throws SQLException {return parseMoney(callableStatement.getLong(i));}
}
5.分页插件
PageHelper 是一个简单且易用的 MyBatis 分页插件。它的设计思想是只对紧跟在 PageHelper.startPage
方法后的第一个 MyBatis 查询方法进行分页。这就意味着如果再次调用查询方法,它就会返回所有的记录,而不是一个分页结果。PageHelper-Spring-Boot-Starter 是 PageHelper 与 Spring Boot 的集成。
PageHelper 官方 GItHub 地址:https://github.com/pagehelper/Mybatis-PageHelper
Spring Boot Starter GItHub 地址:https://github.com/pagehelper/pagehelper-spring-boot
首先,你需要在项目的 pom.xml
文件中添加 pagehelper-spring-boot-starter
的依赖:
<!--MyBatis分页插件-->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper-starter.version}</version>
</dependency>
注意:上面的
${pagehelper-starter.version}
需要根据实际需求选择对应的版本。
然后在 Spring Boot 的配置文件中进行 PageHelper 相关的基本配置:
# PageHelper 分页插件配置
pagehelper:# 配置数据库方言helper-dialect: mysql# 配置分页合理化参数reasonable: true# 配置支持通过 Mapper 接口参数来传递分页参数support-methods-arguments: true# 配置参数映射,即从 Map 中根据指定的名字取值,用于从 Map 中取值时的 keyparams: count=countSql# 如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果page-size-zero: true
常用的配置项如下:
配置项 | 说明 |
---|---|
pagehelper.helper-dialect | 配置数据库的方言 |
pagehelper.page-size-zero | 如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果 |
pagehelper.reasonable | 配置分页合理化参数,默认值为 false。当该参数设置为 true 时,pageNum<=0 会查询第一页,pageNum>pages (超过总页数时)会查询最后一页。默认 false 时,直接根据参数进行查询。 |
pagehelper.support-methods-arguments | 支持通过 Mapper 接口方法参数来传递分页参数,默认值false ,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会进行分页。 |
pagehelper.params | 为了支持 startPage(Object params) 方法,增加了该配置来配置参数映射,用于从对象中根据属性名取值(一般为 Map 中根据指定的名字取值,用于从 Map 中取值时的 key),可以配置 pageNum ,pageSize ,count ,pageSizeZero ,reasonable ,不配置映射的用法可以参考 startPage(Object params) 方法的示例。 |
然后,在 Mapper 接口中,你可以直接进行分页查询:
@Mapper
public interface UserMapper {@Select("select * from user")List<User> selectAll();
}
在 Service 层,你可以通过调用 PageHelper.startPage
方法实现分页:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;/*** 查询所有用户(分页)* @param pageNum 当前页码* @param pageSize 每页大小* @return PageInfo<User>*/public PageInfo<User> selectAll(int pageNum, int pageSize) {// 开启分页(将会自动拦截到下面这查询sql)PageHelper.startPage(pageNum, pageSize);// 执行查询List<User> users = userMapper.selectAll();// 封装为PageInfo对象PageInfo<User> pageInfo = new PageInfo<>(users);// 返回return pageInfo;}
}
在这个示例中,我们首先调用了 PageHelper.startPage
方法,然后调用了 userMapper.selectAll
方法。PageHelper 会对这个方法进行分页,然后我们将结果包装成 PageInfo
对象并返回。
PageInfo
是一个包含了分页信息的对象,包括当前页码、每页的数量、总记录数、总页数、是否为第一页、是否为最后一页、是否有前一页、是否有下一页等。
PageHelper 底层实现分页的原理:
PageHelper 插件的实现是基于 MyBatis 的拦截器接口
Interceptor
。它会对执行 SQL 操作的StatementHandler
进行拦截。在进行 SQL 查询之前,插件会改写要执行的 SQL,加入对应数据库的分页查询语句(即 limit 条件),从而实现物理分页的效果。具体来说,使用 PageHelper 插件时,当调用PageHelper.startPage(pageNum, pageSize)
方法后,会创建一个Page
对象并保存到本地线程变量ThreadLocal
中,因此操作需要在一个线程中。在执行查询之前,会取出Page
对象的信息,并利用这些信息改写 SQL。在查询完成后,将分页信息清空。方法源码如下:
/*** 分页查询* @param pageNum 当前页* @param pageSize 每页大小* @return 一个经过分页后的 Page 对象*/ public static <E> Page<E> startPage(int pageNum, int pageSize) {// 调用下面的 startPage 方法,传入 pageNum、pageSize、DEFAULT_COUNT 参数,返回一个 Page<E> 对象return startPage(pageNum, pageSize, DEFAULT_COUNT); } /*** 真正的分页方法* @param pageNum 当前页* @param pageSize 每页大小* @param count 数据表中的记录总数,用于计算分页的页数和当前页的数据数量,以便于实现分页功能* @return 一个经过分页后的 Page 对象*/ public static <E> Page<E> startPage(int pageNum, int pageSize, int count) {// 创建 Page 对象Page<E> page = new Page<>(pageNum, pageSize, count);// 将 Page 对象设置到 ThreadLocal 中,方便在同一线程的任意地方获得 Page 对象PAGE_LOCAL.set(page);// 返回 Page 对象return page; }
当然,MyBatis 本身其实也提供了 RowBounds
对象和在 Mapper 方法参数中指定分页信息的方式进行分页。RowBounds
是 MyBatis 提供的一个用于物理分页的类,它有两个重要的属性,offset
和 limit
。其中 offset
表示开始读取的位置(偏移量),limit
表示读取的数量。例如,new RowBounds(10, 20)
表示从第10条记录开始,读取20条记录。
注意:当我们配置了
pagehelper.offset-as-page-num=true
后会将offset
当成pageNum
页码使用,第二个参数limit
为pageSize
参数。
使用 RowBounds
对象的例子:
@Select("select * from user")
List<User> selectAllByRowBounds(RowBounds rowBounds);
在方法参数中指定分页信息的例子:
/*** 查询所有用户(分页)* @param pageNum 当前页码* @param pageSize 每页大小* @return PageInfo<User>*/
public PageInfo<User> selectAllByRowBounds(int pageNum, int pageSize) {// 执行查询List<User> users = userMapper.selectAllByRowBounds(new RowBounds(pageNum, pageSize));// 封装为PageInfo对象PageInfo<User> pageInfo = new PageInfo<>(users);// 返回return pageInfo;
}
PageHelper VS RowBounds:
- PageHelper 是基于 MyBatis 插件实现的,只需要在查询前调用
PageHelper.startPage
方法即可实现分页,无需修改原有的 SQL。它会自动识别数据库类型,并生成对应的分页 SQL。PageHelper 只返回需要的记录,不必查询所有数据,性能较好。- RowBounds 是 MyBatis 提供的一个用于分页的对象,通过查询所有数据后,再返回指定范围的记录实现的,所以在数据量较大时,性能较差。
因此,大多数情况下,推荐使用 PageHelper 插件进行分页,不仅可以减少代码的复杂度,而且更加易用。