示例代码:https://github.com/GiraffePeng/mybatis-simple
1、MyBatis 最佳实践
1.1、动态 SQL
1.1.1、为什么需要动态 SQL?
项目实际开发中,由于前台传入的查询参数不同,需要写很多的 if else,还需要非常注意 SQL 语句里面的 and、空格、逗号和转移的单引号这些,拼接和调试 SQL 就是一件非常耗时的工作。
MyBaits 的动态 SQL 就帮助我们解决了这个问题,它是基于 OGNL 表达式的。
1.1.2、动态标签有哪些?
按照官网的分类,MyBatis 的动态标签主要有四类:if,choose (when, otherwise), trim (where, set),foreach。
1.1.2.1、if标签
需要判断的时候,条件写在 test 中。下面的例子,当参数中的name不为空,则拼接上根据name查询数据的sql
<select id="selectListByPage" resultMap="BaseResultMap" parameterType="map">
select
<include refid="Base_Column_List" />
from member
where 1=1
<if test="name != null and name != ''">
and name = #{name,jdbcType=VARCHAR}
</if>
</select>
1.1.2.2、choose (when, otherwise)标签
choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满则时,则执行 otherwise 中的sql。类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。
下面的例子,比如要根据创建时间的开始时间和结束时间查询member,并且开始时间和结束时间有可能不全部有值,我们可以采用choose。
<select id="selectListByPage" resultMap="BaseResultMap" parameterType="map">
select
<include refid="Base_Column_List" />
from member
<where>
<choose>
<when test="startDate != null and endDate == null">
and create_time > #{startDate}
</when>
<when test="startDate == null and endDate != null">
and #{endDate} > create_time
</when>
<when test="startDate != null and endDate != null">
and create_time between #{startDate} and #{endDate}
</when>
<otherwise>
</otherwise>
</choose>
</where>
</select>
1.1.2.3、trim (where, set)
需要去掉 where、and、逗号之类的符号的时候。
下面的例子,trim 用来指定或者去掉前缀或者后缀,同时注意最后一个条件 detail 多了一个逗号,就是用 trim 去掉的:
<insert id="insertSelective" parameterType="com.peng.entity.Member" >
insert into member
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="id != null" >
id,
</if>
<if test="name != null" >
name,
</if>
<if test="text != null" >
phone,
</if>
<if test="detail != null">
detail,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="id != null" >
#{id,jdbcType=BIGINT},
</if>
<if test="name != null" >
#{name,jdbcType=VARCHAR},
</if>
<if test="phone != null" >
#{phone,jdbcType=VARCHAR},
</if>
<if test="detail != null">
#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler},
</if>
</trim>
</insert>
1.1.2.4、foreach
需要遍历集合的时候。下面以批量插入数据为例。最终的拼装好的sql为:
insert into member(name,phone,detail) values (?,?,?),(?,?,?),(?,?,?);
<insert id="insertBatch" parameterType="list">
insert into member(name,phone,detail)
values
<foreach collection="list" item="item" separator=",">
(
#{name,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR},
#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
)
</foreach>
</insert>
动态 SQL 主要是用来解决 SQL 语句生成的问题。
1.2、批量操作
我们在生产的项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况 ,当数据量非常大,比如超过几万条的时候,在 Java 代码中循环发送 SQL 到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话,即使我们使用了数据库连接池技术,对于数据库服务器来说也是不堪重负的。在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们生成语法正确的 SQL 语句。
1.2.1、批量插入
批量插入的语法是这样的,只要在 values 后面增加插入的值就可以了。
insert into member(name,phone,detail) values (?,?,?),(?,?,?),(?,?,?);
在 Mapper 文件里面,我们使用 foreach 标签拼接 values 部分的语句:
<insert id="insertBatch" parameterType="list">
insert into member(name,phone,detail)
values
<foreach collection="list" item="item" separator=",">
(
#{name,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR},
#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
)
</foreach>
</insert>
Java 代码里面,直接传入一个 List 类型的参数。 效率要比循环发送 SQL 执行要高得多。最关键的地方就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。
1.2.2、批量更新
批量更新的语法是这样的,通过 case when,来匹配 id 相关的字段值。
UPDATE member
SET name = CASE id
WHEN ? THEN
?
WHEN ? THEN
?
WHEN ? THEN
?
END,
phone = CASE id
WHEN ? THEN
?
WHEN ? THEN
?
WHEN ? THEN
?
END,
detail = CASE id
WHEN ? THEN
?
WHEN ? THEN
?
WHEN ? THEN
?
END
WHERE
id IN (?, ?, ?);
利用mapper中的foreach标签来拼接sql。
<update id="updateBatch" parameterType="list">
update member set
name =
<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
when #{item.id} then #{item.name}
</foreach>
,phone =
<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
when #{item.id} then #{item.phone}
</foreach>
,detail =
<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
when #{item.id} then #{item.detail,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
</foreach>
where id in
<foreach collection="list" item="item" index="index" separator="," open="(" close=")" >
#{item.id}
</foreach>
</update>
批量删除也是类似的。
1.2.3、Batch Executor
当然 MyBatis 的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的 SQL 语句过大。
MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置才可以解决这个问题。
Caused by : com.mysql.jdbc.PacketTooBigException:Packet for query is too large
(7188967 >4194304). You can change this value on the server by setting the
max_allowed_packet' variable.
在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种 BatchExecutor。
<setting name="defaultExecutorType" value="BATCH" />
也可以在创建会话的时候指定执行器类型:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
BatchExecutor 底层是对 JDBC ps.addBatch()的封装,原理是攒一批 SQL 以后再发送。
那么三种执行器(Simple、Reuse、Batch)的区别是什么?
- SimpleExecutor:是普通的执行器,在进行Mapper.save()方法时,相当于JDBC的stmt.execute(sql)。
- ReuseExecutor:会重用预处理语句(prepared statements),在进行Mapper.save()方法时,相当于JDBC重用一条sql,再通过stmt传入多项参数值,然后执行stmt.executeUpdate()或stmt.executeBatch()
- BatchExecutor:将重用语句并执行批量更新,在进行Mapper.save()方法时,相当于JDBC语句的 stmt.addBatch(sql),即仅仅是将执行SQL加入到批量计划。 所以此时不会抛出主键冲突等运行时异常,而只有临近commit前执行stmt.execteBatch()后才会抛出异常。
1.3、嵌套(关联)查询
我们在查询业务数据的时候经常会遇到跨表关联查询的情况,
比如想查询用户并带出该用户的消费记录(一对多),或者查询用户并查询出该用户的钱包余额(一对一)。
我们映射结果有两个标签,一个是 resultType,一个是 resultMap。
resultType 是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字段无法直接映射,就要用 resultMap 来建立映射关系。
对于关联查询的这种情况,通常不能用 resultType 来映射。用 resultMap 映射,要么就是修改 DTO(Data Transfer Object),在里面增加字段,这个会导致增加很多无关的字段。要么就是引用关联的对象,比如 Member里面包含了一个 PurchaseLog 对象,这种情况下就要用到关联查询(collection,或者association),MyBatis 可以帮我们自动做结果的映射。
一对多的关联查询有两种配置方式:
1.3.1、嵌套结果
<resultMap id="BaseResultMap" type="com.peng.entity.Member" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="phone" property="phone" jdbcType="VARCHAR" />
<result column="detail" property="detail" jdbcType="VARCHAR" typeHandler="com.peng.mybatis.typehandler.JsonTypeHandler"/>
<!-- 嵌套结果 -->
<collection property="purchaseLogs" javaType="java.util.ArrayList" ofType="com.peng.entity.PurchaseLog">
<result column="purchase_price" property="purchasePrice" jdbcType="DECIMAL" />
<result column="purchase_name" property="purchaseName" jdbcType="VARCHAR" />
</collection>
</resultMap>
<!-- 验证嵌套结果查询 -->
<select id="selectList" resultMap="BaseResultMap">
select a.id, a.name, a.phone, a.detail, b.purchase_price, b.purchase_name
from member a
left join purchase_log b
on a.id = b.member_id
</select>
1.3.2、嵌套查询
<resultMap id="BaseResultQueryMap" type="com.peng.entity.Member" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="phone" property="phone" jdbcType="VARCHAR" />
<result column="detail" property="detail" jdbcType="VARCHAR" typeHandler="com.peng.mybatis.typehandler.JsonTypeHandler"/>
<!--
嵌套查询,会出现N+1的问题
使用collection标明一对多的关系,通过声明的select中的内容,会在调用完member表的查询后,
再去调用com.peng.mybatis.mapper.PurchaseLogMapper.selectListByMemberId
方法来完成关联表的查询
-->
<collection property="purchaseLogs" javaType="java.util.ArrayList" ofType="com.peng.entity.PurchaseLog"
select="com.peng.mybatis.mapper.PurchaseLogMapper.selectListByMemberId" column="{memberId=id}">
</collection>
</resultMap>
嵌套查询由于是分两次查询,当我们查询了用户信息之后,会再发送一条 SQL 到数据库查询用户消费记录信息。
我们只执行了一次查询用户信息的 SQL(所谓的 1),如果返回了 N 条记录,就会再发送 N 条到数据库查询用户消费记录的信息(所谓的 N),这个就是我们所说的 N+1 的问题。 这样会白白地浪费我们的应用和数据库的性能。
如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用用户消费记录的信息的时候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。这个可以参考上一篇的延迟加载部分:MyBatis应用分析与最佳实践(一)。
1.4、MBG 与 Example
https://github.com/mybatis/generator
我们在项目中使用 MyBaits 的时候,针对需要操作的一张表,需要创建实体类、 Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,这部分的工作是非常繁琐的。而大部分时候我们对于表的操作是相同的,比如根据主键查询、根据 Map 查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着有大量的重复工作。所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper 映射器、Mapper 接口,里面包含了我们需要用到的这些基本方法和 SQL 呢?
MyBatis 提供了MyBatis Generator,简称 MBG。我们只需要修改一个配置文件,使用相关的 jar 包命令或者 Java 代码就可以帮助我们生成实体 类、映射器和接口文件。
MBG 的配置文件里面有一个 Example 的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成 where 条件。
原理:在实体类中包含了两个有继承关系的 Criteria,用其中自动生成的方法来构建查询条件。把这个包含了 Criteria 的实体类作为参数传到查询参数中,在解析 Mapper 映射器的时候会转换成 SQL 条件。
以member表为例,通过MBG生成后的类包括:
- Member:对应表的实体类
- MemberExample:相当于一个查询构建器,里面持有List<Criteria> oredCriteria的属性,Criteria相当于where参数的构建器,通过memberExample.createCriteria()方法来创建出Criteria,以member表为例,Criteria含有方法如下图所示:
我们看到是对表里面的字段封装的 ==,isNUll,isNotNull,like、not like、>、<等等。 - MemberMapper.java:里面声明了基本的增删改查的方法。
- MemberMapper.xml:自动生成了mapper.xml文件,里面包含基本的增删改查的sql声明。值得提到的是里面的查询方法selectByExample,代码如下:
<sql id="Example_Where_Clause" >
<where >
<foreach collection="oredCriteria" item="criteria" separator="or" >
<if test="criteria.valid" >
<trim prefix="(" suffix=")" prefixOverrides="and" >
<foreach collection="criteria.criteria" item="criterion" >
<choose >
<when test="criterion.noValue" >
and ${criterion.condition}
</when>
<when test="criterion.singleValue" >
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue" >
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue" >
and ${criterion.condition}
<foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.sina.pojo.MemberExample" >
select
<if test="distinct" >
distinct
</if>
<include refid="Base_Column_List" />
from member
<if test="_parameter != null" >
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null" >
order by ${orderByClause}
</if>
</select>
可以看到,通过我们传入的MemberExample,MBG通过引入动态SQL:Example_Where_Clause,来完成where相关字段的条件查询。其中_parameter 是mybatis内置的参数, _parameter:代表整个参数,单个参数:_parameter就是这个参数。多个参数:参数会被封装为一个map:_parameter就是代表这个map。 上面的代码 _parameter指得就是 MemberExample,目的在于比较通用的判断是否传递了Example。
根据用户的id查询用户信息,我们实际使用可以这么写:
MemberMapper mapper = session.getMapper(MemberMapper.class);
MemberExample memberExample = new MemberExample();
Criteria criteria = memberExample.createCriteria();
criteria.andIdEqualTo(1L);
List<Member> memberList = mapper.selectByExample(memberExample);
System.out.println(memberList.get(0).getName());
生成的语句:
select id, name, phone, detail from member WHERE ( id = ? )
1.5、翻页
在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。
逻辑翻页与物理翻页
在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物理翻页是真正的翻页,比如 MySQL 使用 limit 语句,Oracle 使用 rownum 语句,SQL Server 使用 top 语句。
1.5.1、逻辑翻页
MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和 limit(从第几条开始,查询多少条)。
我们可以在 Mapper 接口的方法上加上这个参数,不需要修改 xml 里面的 SQL 语句。
声明查询方法:
public List<Member> selectListByRowBounds(RowBounds rowBounds);
<select id="selectListByRowBounds" resultMap="BaseResultMap" >
select
<include refid="Base_Column_List" />
from member
</select>
MemberMapper mapper = session.getMapper(MemberMapper.class);
//从第一页查询,查询2条
RowBounds rowBounds = new RowBounds(0, 2);
List<Member> members = mapper.selectListByRowBounds(rowBounds);
System.out.println(members.size());
它的底层其实是对 ResultSet 的处理。它会舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条。
public class DefaultResultSetHandler implements ResultSetHandler {
...
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
...
}
很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用 subList(start,end)没什么区别),所以我们要用到物理翻页。
1.5.2、物理翻页
物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
第一种简单的办法就是传入参数(或者包装一个 page 对象),在 SQL 语句中翻页。
<select id="selectList" resultMap="BaseResultMap" parameterType="map">
select
<include refid="Base_Column_List" />
from member
limit #{current}, #{size}
</select>
第一个问题是我们要在 Java 代码里面去计算起止序号;第二个问题是:每个需要翻页的 Statement 都要编写 limit 语句,会造成 Mapper 映射器里面很多代码冗余。
那我们就需要一种通用的方式,不需要去修改配置的任何一条 SQL 语句,只要在我们需要翻页的地方封装一下翻页对象就可以了。
我们最常用的做法就是使用翻页的插件,这个是基于 MyBatis 的拦截器实现的,比如 PageHelper。
MemberMapper mapper = session.getMapper(MemberMapper.class);
//物理分页
PageHelper.startPage(0, 10);
List<Member> members = mapper.selectListByPageHelper();
//计算分页所需参数 比如 总页数,总条数,是否有下一页等等
PageInfo<Member> pageInfo = new PageInfo<Member>(members);
System.out.println(JSON.toJSONString(pageInfo));
别忘了配置分页插件,我们是使用的原生mybatis,需要指定插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
PageHelper 是通过 MyBatis 的拦截器实现的,简单地来说,它会根据 PageHelper 的参数,改写我们的 SQL 语句。比如 MySQL 会生成 limit 语句,Oracle 会生成 rownum 语句,SQL Server 会生成 top 语句。
我们可以看到在mapper的方法上并没有声明任何的参数,他为什么就会执行相应的物理分页的,首先我们查看PageHelper.startPage(0, 10)的源码
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
//将page对象保存在threadLocal中
setLocalPage(page);
return page;
}
}
发现PageHelper将我们传递的当前页和一页多少条存入在Page对象里,同时把Page对象放入在ThreadLocal中。接着我们去看PageHelper声明的分页插件.
通过拦截mybatis的Executor执行器的query方法。
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
......
@Override
public Object intercept(Invocation invocation) throws Throwable {
...
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//调用ExecutorUtil的pageQuery方法来执行查询
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
}else{
//不进行分页处理
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
...
}
}
首先可以看到先是判断了是否需要分页调用dialect.skip(ms, parameter, rowBounds)方法,我们点进去查看方法。
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
....
//这里就是从刚才的ThreadLocal里获取page对象,
//如果存在,代表本次查询是有分页需求的
//如果为null,证明没有声明PageHelper.startPage(0, 10);不执行分页查询
Page page = pageParams.getPage(parameterObject, rowBounds);
if (page == null) {
return true;
} else {
//设置默认的 count 列
if (StringUtil.isEmpty(page.getCountColumn())) {
page.setCountColumn(pageParams.getCountColumn());
}
autoDialect.initDelegateDialect(ms);
return false;
}
}
回到分页本身可以看到最终调用的为ExecutorUtil的pageQuery方法,我们看ExecutorUtil的源码。
public abstract class ExecutorUtil {
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//这里根据不同的数据库拼接不同的分页语句,
//最后将原有的查询sql后拼接分页关键字,得到新的sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
}
我们进入dialect.getPageSql()方法查询是如何拼接分页语句的
public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {
...
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
//在这里,通过threadLocal中获取Page对象
Page page = getLocalPage();
//支持 order by
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
if (page.isOrderByOnly()) {
return sql;
}
//这里声明了一个抽象方法
return getPageSql(sql, page, pageKey);
}
...
}
我们看到先去threadLocal中获取Page对象,然后调用getPageSql(sql, page, pageKey)的抽象方法,我们查看getPageSql的实现类,发现根据数据库类型的不同,有不同的实现类,以我们常见的mysql为例,我们进入MysqlDialect.
public class MySqlDialect extends AbstractHelperDialect {
...
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
...
}
可以看到,就是通过在原有的sql上追加LIMIT关键字来完成的物理分页的逻辑。至此,我们可以了解到PageHelper的相关实现逻辑。至于为什么通过声明一个mybatis插件,在相关的方法执行前会被拦截呢,后续会提到。
1.6、Mybatis-plus
https://mybatis.plus/guide
MyBatis-Plus 是原生 MyBatis 的一个增强工具,可以在使用原生 MyBatis 的所有功能的基础上,使用 plus 特有的功能。
MyBatis-Plus的核心功能:
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询 - 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
Mybatis-plus章节内容来源自:https://mybatis.plus/guide