MybatisPlus Advanced Guide
前言
目前,市面大多数公司都会使用MybatisPlus开发,但是遇到一些比较复杂的sql,大家依然会选择xml的方式来书写sql,本文意挖掘MybatisPlus的高级功能,实现相对比较复杂的sql以及sql的自定义化。
附:MybatisPlus基础部分的使用方式可以参考官方文档 MybatisPlus文档
文章目录
部分实体字段更新及查询
查询部分实体字段值
// 第一类
select(String... sqlSelect)
// 第二类
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
第一类方法直接查询对应的字段
select("id", "name", "age") // 普通查询
select(PatientDO::getPatientName, PatientDO::getPinyin) // lambda查询
第二类方法为:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值!这两类方法重复调用以最后一次为准
select(i -> i.getProperty().startsWith("marco"))
更新部分实体字段值
// 第一类
set(String column, Object val)
set(boolean condition, String column, Object val)
// 第二类
setSql(boolean condition, String sql)
第一类逐个设置更新实体字段
default void updatePatientName(PatientDO patientDO) {
update(patientDO, new LambdaUpdateWrapper<PatientDO>()
.set(PatientDO::getPatientName, patientDO.getPatientName()));
};
第二类类似于直接写sql,不过值需要写死,不太推荐
setSql(true, "patientName = 'marco'")
以上这两种方式适用于置空某一字段
自定义SQL
当有些场景需要自定义sql,供所有方法使用时,mybatis-plus提供了如下两种方式
譬如说我现在需要定义一个公用的BaseMapper方法,其他的Mapper去继承BaseMapper
baseMapper.getAll(Wrappers.<PatientDO>lambdaQuery().eq(PatientDO::isDeleted, 1));
注解方式
@Select("select * from patient ${ew.customSqlSegment}")
List<PatientDO> getAll(@Param(Constants.WRAPPER) Wrapper wrapper);
Xml方式
<select id="getAll" resultType="MysqlData">
SELECT * FROM entity ${ew.customSqlSegment}
</select>
链式CURD(仅供了解
)
前面的查询都是创建一个条件构造器,再通过mapper或service以条件构造器为参数进行查询。例如LambdaQueryChainWrapper将前面的两句合二为一,一句来完成带条件的查询。
链式查询
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery();
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();
链式更新
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);
注意:创建LambdaQueryChainWrapper对象需要传递mapper对象,还有个缺点链式查询只有list()和one()两个方法获取数据,返回的都是实体对象或实体对象的集合,没有对应的Map()查询方法。
@Autowired
private CustomerMapper customerMapper;
public void selectByCustomerName() {
List<CustomerDO> list = new LambdaQueryChainWrapper<CustomerDO>(customerMapper)
.select(CustomerDO::getId, Customer::getName)
.like(CustomerDO::getName, "marco").list();
BaseMapper拓展通用Sql
第一步:定义通用sql语句的method
/**
* 删除全部
*/
public class DeleteAll extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
/* 执行 SQL ,动态 SQL 参考类 SqlMethod */
String sql = "delete from " + tableInfo.getTableName();
/* mapper 接口方法名一致 */
String method = "deleteAll";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, method, sqlSource);
}
}
第二步:定义并注入MySqlInjector
/**
* 自定义Sql注入
*
* @author nieqiurong 2018/8/11 20:23.
*/
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
//增加自定义方法
methodList.add(new DeleteAll());
/**
* 以下 3 个为内置选装件
* 头 2 个支持字段筛选函数
*/
// 例: 不要指定了 update 填充的字段
methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
methodList.add(new AlwaysUpdateSomeColumnById());
methodList.add(new LogicDeleteByIdWithFill());
return methodList;
}
}
@Configuration
public class MybatisPlusConfig {
@Bean
public MySqlInjector sqlInjector() {
return new MySqlInjector();
}
}
第三步:自定义sql的方法接口
public interface MyBaseMapper<T> extends BaseMapper<T> {
/**
* 以下定义的 4个 method 其中 3 个是内置的选装件
*/
int insertBatchSomeColumn(List<T> entityList);
int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY) T entity);
int deleteByIdWithFill(T entity);
/**
* 以下为自己自定义
*/
int deleteAll();
}
使用@SelectProvider注入复杂查询
定义查询条件PatientSelectProvider
单个参数
public class PatientSelectProvider {
public String selectById(String patientId) {
return new SQL(){{
SELECT("patient_id, patient_name");
FROM("patient");
WHERE("patient_id ="+ patientId);
}}.toString();
}
}
在Mapper中引用Provider
@SelectProvider(value = PatientSelectProvider.class, method = "selectById")
PatientDO selectByPatientId();
注意:当mapper中传入的参数是使用@param 注解修饰,在xxxProvider类中必须使用Map对象接收参数。
多个参数
public String selectUserById(Map<String, Object> para){
return new SQL(){{
SELECT("patient_id, patient_name");
FROM("patient");
WHERE("patient_id=" + para.get("patientId"));
if(StringUtils.isNotBlank((String)para.get("patientName"))){
WHERE("patientName=" + para.get("patientName"));
}
}}.toString();
}
注意:
此时的sql写法在拼接sql中不需要在使用 and 进行连接 ,在where 方法中已拼入where 源码如下
private static final String AND = ") \nAND (";
private static final String OR = ") \nOR (";
函数式sql编程
func
func(Consumer<Children> consumer)
func(boolean condition, Consumer<Children> consumer)
func 方法(主要方便在出现if…else下调用不同方法能不断链)
func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})
or
// 拼接
or()
or(boolean condition)
// 嵌套
or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
拼接 OR
eq("id",1).or().eq("name","marco")
// -- 等同于 -> id = 1 or name = 'marco'
注意事项:主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)
嵌套 OR
or(i -> i.eq("name", "marco").ne("status", "1"))
// -- 等同于 -> or (name = 'marco' and status <> '1')
and
and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
and(i -> i.eq("name", "marco").ne("status", "1"))
// -- 等同于 -> and (name = 'marco' and status <> '1')
apply
apply(String applySql, Object... params)
apply(boolean condition, String applySql, Object... params)
注意事项:
该方法可用于数据库函数 动态入参的params对应前面applySql内部的{index}部分,这样是不会有sql注入风险的,反之会有~(建议使用最后一种方式)
apply("id = 1")
// -- 等同于 -> id = 1
apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
// -- 等同于 -> date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")
// -- 等同于 -> date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
last
last(String lastSql)
last(boolean condition, String lastSql)
注意事项:
只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
// 常用方式如下
last("limit 1")
FieldFill注解
针对于需要新增填充的字段,在实体的字段上加上
@TableField(fill = FieldFill.INSERT)
private Date createTime
针对于需要新增或者填充的字段,在实体的字段上加上
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime
连表查询
建议使用xml来连表查询,当然也可以使用以下几种方式进行连表查询
注解方式
//多表联合查询 按条件orderID
@Select("select t1.*,t2.user_name,t2.nick_name from orders t1 LEFT JOIN users t2 ON t1.user_id =t2.id WHERE t1.user_id= #{id}")
List<Map<String,Object>> orderUserList(Page<Map<String,Object>> page, String id);
}
SelectProvider外部拓展方式
首先还是定义SelectProvider实体
public String selectByPatientId(Long patientId) {
return new SQL(){{
SELECT("p.patient_id, p.patient_name, pc.mobile");
FROM("patient p");
LEFT_OUTER_JOIN("patient_contacts pc on p.patient_id = pc.patient_id");
WHERE("p.patient_id = " + patientId);
}}.toString();
}
接着还是在Mapper中引用Provider
@SelectProvider(value = PatientSelectProvider.class, method = "selectByPatientId")
PatientDO selectByPatientId(Long patientId);
乐观锁插件
乐观锁实现方式: 取出记录时,获取当前version 更新时,带上这个version 执行更新时,
set version = newVersion where version = oldVersion
如果version不对,就更新失败 乐观锁配置需要2步 记得两步
1、插件配置
首先注入乐观所拦截器OptimisticLockerInterceptor
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
2、注解实体字段 @Version
必须要!
@Version
private Integer version;
特别说明:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime 整数类型下
newVersion = oldVersion + 1 newVersion 会回写到 entity 中仅支持
updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper)方法下, wrapper 不能复用!!!
int id = 100;
int version = 2;
User u = new User();
u.setId(id);
u.setVersion(version);
...
u.setXXX(xxx);
if(userService.updateById(u)){
System.out.println("Update successfully");
}else{
System.out.println("Update failed due to modified by others");
}
// -- 等同于 -> update tbl_user set name = 'update',version = 3 where id = 100 and version = 2