分页功能实现
自定义的Mapper接口的继承于BaseMapper,这样进行分页时只需要调用BaseMapper接口的selectPage()函数即可,样例代码如下:
// 自定义的Mapper接口,继承于BaseMapper<>接口
public interface ProdValuationMapper extends BaseMapper<ProdValuation> {
List<ProdValuationIndicatorDTO> getProdValuationIndicators(@Param("query") ProductValuationQuery query);
}
调用ProdValuationMapper的selectPage()函数进行分页查询的代码如下:
@Component
public class ProdRiskHelper {
@Autowired
private ProdValuationMapper prodValuationMapper;
/**
* 获取最新日期
*
* @param portfolioNos 组合编号
* @param endDate 结束日期
* @return 最新日期
*/
public LocalDate getLastValuationdate(List<String> portfolioNos, LocalDateTime endDate) {
// 创建一个LambdaQueryWrapper对象,用于封装业务查询条件和排序规则
final LambdaQueryWrapper<ProdValuation> wrapper = new LambdaQueryWrapper<>(ProdValuation.class);
wrapper.in(ProdValuation::getProductno, productNoList)
.le(ProdValuation::getValuationdate, endDate)
.orderByDesc(ProdValuation::getValuationdate);
// 定义分页查询的参数信息,设置从0开始的第一条数据
final Page<ProdValuation> page = new Page<>(0, 1);
// 调用BaseMapper对象的selectPage()函数获取分页后的数据
final Page<ProdValuation> prodValuationPage = prodValuationMapper.selectPage(page, wrapper);
if (CollectionUtils.isNotEmpty(prodValuationPage.getRecords())) {
if (prodValuationPage.getRecords().get(0).getValuationdate() != null) {
return prodValuationPage.getRecords().get(0).getValuationdate().toLocalDate();
}
}
return null;
}
必须要确保自定义的Mapper接口继承Mybatis-plus的BaseMapper接口,然后才能调用BaseMapper的分页函数。BaseMapper接口中关于分页的查询函数如下:
/**
* Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
* <p>这个 Mapper 支持 id 泛型</p>
*
* @author hubin
* @since 2016-01-23
*/
public interface BaseMapper<T> extends Mapper<T> {
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @since 3.5.3.2
*/
List<T> selectList(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @param resultHandler 结果处理器 {@link ResultHandler}
* @since 3.5.4
*/
void selectList(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper, ResultHandler<T> resultHandler);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
* @since 3.5.3.2
*/
List<Map<String, Object>> selectMaps(IPage<? extends Map<String, Object>> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
* @param resultHandler 结果处理器 {@link ResultHandler}
* @since 3.5.4
*/
void selectMaps(IPage<? extends Map<String, Object>> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper, ResultHandler<Map<String, Object>> resultHandler);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
<E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
*/
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}
我们知道MyBatis-plus必须要实现BaseMapper接口的分页函数才可以,参考MyBatis源码解析——SqlSession执行Mapper过程,MyBatis中通过MapperProxy类实现动态代理,在Mybatis-plus中,也使用MapperProxy类实现动态代理的。下面是MapperProxy类的关键代码:
public class MybatisMapperProxy<T> implements InvocationHandler, Serializable {
// SQL会话
private final SqlSession sqlSession;
// Mapper接口
private final Class<T> mapperInterface;
// Mapper接口中的方法缓存,Key为Mapper中的方法,Value为Mapper中方法的调用者
private final Map<Method, MapperMethodInvoker> methodCache;
public MybatisMapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果当前是一个Object对象,直接执行其方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//这里会把当前调用的方法封装成一个MapperMethodInvoker然后再调用其invoke方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return CollectionUtils.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//正常走这里
return new PlainMethodInvoker(new MybatisMapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
在mybitis中MapperMethodInvoker
实例时传的是MapperMethod对象实例,而在mybatis-plus中传的是MybatisMapperMethod实例,两者在逻辑上差异不大,主要是在execute()函数中针对<select>标签增加了分页处理逻辑,源码如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
// 判断返回值类型是不是IPage或其子类
if (IPage.class.isAssignableFrom(method.getReturnType())) {
// 执行分页查询
result = executeForIPage(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
可以看到,mybatis-plus针对Mapper接口中函数返回值类型为IPage或其子类的查询操作都会执行executeForIPage()函数,executeForIPage函数的代码逻辑如下:
@SuppressWarnings("all")
private <E> Object executeForIPage(SqlSession sqlSession, Object[] args) {
IPage<E> result = null;
for (Object arg : args) {
// 查询类型为IPage或其子类的参数
if (arg instanceof IPage) {
result = (IPage<E>) arg;
break;
}
}
Assert.notNull(result, "can't found IPage for args!");
// 调用MapperMethod.MethodSignature对象的函数convertArgsToSqlCommandParam(),将参数转换为SQL命令的参数
Object param = method.convertArgsToSqlCommandParam(args);
// 调用SqlSession的selectList()获取数据
List<E> list = sqlSession.selectList(command.getName(), param);
result.setRecords(list);
return result;
}
上述代码会调用MapperMethod.MethodSignature对象的函数convertArgsToSqlCommandParam(),将参数转换为SQL命令的参数,然后再执行SqlSession的selectList()函数获取数据,其中convertArgsToSqlCommandParam()函数代码如下:
public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final boolean returnsCursor;
private final boolean returnsOptional;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver;
public Object convertArgsToSqlCommandParam(Object[] args) {
return this.paramNameResolver.getNamedParams(args);
}