为了账号安全,请及时绑定邮箱和手机立即绑定

MyBatis Plus学习笔记(八)

标签:
Java SSM

image-20210203115548602

8. 插件扩展

8.1. 分页插件

之前做列表或者报表功能时使用最多的是pageHelper插件做Mybatis的分页查询,MP为了方便将分页做了一个相关的模块com.baomidou.mybatisplus.extension.plugins.pagination供我们分页查询时去使用.

8.1.1. 配置插件

在spring的配置文件中,在sqlsessionFactory里面增加插件相关的配置,增加分页插件即可。

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
    <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml"></property>
    <!--加入MP的全局配置-->
    <property name="globalConfig" ref="globalConfig"></property>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    <!-- 配置插件 -->
    <property name="plugins">
        <list>
            <!--分页插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor"/>
        </list>
    </property>
</bean>

8.1.2. 测试代码

还记得我们之前做通用CRUD的时候,介绍过selectPage方法,是分页查询的方法。编写测试代码,如下:

@Test
public void pageTest(){
    IPage<User> userIPage = userMapper.selectPage(new Page<>(2, 2), null);
    List<User> userList = userIPage.getRecords();
    userList.forEach(System.out::println);
    System.out.println("当前第"+userIPage.getCurrent()+"页");
    System.out.println("共"+userIPage.getPages()+"页");
    System.out.println("共"+userIPage.getTotal()+"条记录");
    System.out.println("当前页有"+userIPage.getSize()+"条记录");
}

使用分页查询,会先执行插件的cunt语句,如果表里面此时没有数据,就不会执行查询操作。如下:

SELECT COUNT(1)
 FROM user;
------------------------------------------------------------------------------------------------------------------------

我们往表里面添加几条数据,再次执行,就会执行分页查询操作。然后可以通过userIPage对象获取相关的分页信息,如下:

SELECT COUNT(1)
 FROM user;
------------------------------------------------------------------------------------------------------------------------
SELECT id,name,age,email,role_id
 FROM user
 LIMIT 2,2;
------------------------------------------------------------------------------------------------------------------------

控制台的日志输出:

User(id=4, name=Sandy, age=21, email=test4@baomidou.com, isDelete=0)
User(id=5, name=Billie, age=24, email=test5@baomidou.com, isDelete=0)
当前第2页
共2页
共4条记录
当前页有2条记录

注:分页插件会根据数据库不同,会组织不同的分页sql.基本上常见的数据库都支持。

8.1.3. 高级用法(攻击 SQL 阻断解析器)

作用!阻止恶意的全表更新删除

8.1.3.1. 配置分页插件的属性

在分页插件上增加列表属性sqlParserList,配置上攻击 SQL 阻断解析器(com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser)。

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <!-- 配置插件 -->
    <property name="plugins">
        <list>
            <!--分页插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor">
                <property name="sqlParserList" >
                    <list>
                        <!--全表删除更新的SQL阻断拦截器-->
                        <bean class="com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser"></bean>
                    </list>
                </property>
            </bean>
        </list>
    </property>
</bean>

8.1.3.2. 测试全表删除

当执行全表删除的时候,就会报异常,帮助我们拦截危险的sql操作。

image-20210113170306963

8.1.3.3. 攻击 SQL 阻断解析器源码查看

public class BlockAttackSqlParser extends AbstractJsqlParser {

    @Override
    public void processInsert(Insert insert) {
        // to do nothing
    }
    //删除的where条件是空的话会抛出异常
    @Override
    public void processDelete(Delete delete) {
        Assert.notNull(delete.getWhere(), "Prohibition of full table deletion");
    }
    //更新的where条件是空的话会抛出异常
    @Override
    public void processUpdate(Update update) {
        Assert.notNull(update.getWhere(), "Prohibition of table update operation");
    }

    @Override
    public void processSelectBody(SelectBody selectBody) {
        // to do nothing
    }
}

这是两个跟简单的sql拦截规则,我们可以根据自己的需求编写自己的规则,继承AbstractJsqlParser然后配置到插件的sqlParserList里,即可实现阻断拦截功效。

8.1.3.4. 自定义的阻断拦截器

例如我们自定义一个阻断解析器,当查询条件里面有"1=1"条件的时候,进行阻断拦截。

public class MySqlParser extends AbstractJsqlParser {
    @Override
    public void processInsert(Insert insert) {}

    @Override
    public void processDelete(Delete delete) {}

    @Override
    public void processUpdate(Update update) {}

    @Override
    public void processSelectBody(SelectBody selectBody) {
        PlainSelect plainSelect = (PlainSelect) selectBody;
        boolean contains = plainSelect.getWhere().toString().contains("1 = 1");
        System.out.println(contains);
        Assert.isFalse(contains, "可疑的全表查询");
    }
}

配置阻断拦截器

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <!-- 配置插件 -->
    <property name="plugins">
        <list>
            <!--分页插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor">
                <property name="sqlParserList" >
                    <list>
                        <!--自定义的sql阻断拦截器-->
                        <bean class="com.zzlh.mp.common.sqlParser.MySqlParser"/>
                    </list>
                </property>
            </bean>
        </list>
    </property>
</bean>

image-20210113170337379

8.2. 逻辑删除插件

逻辑删除就是将数据的变成不活跃的,不再参与业务当中,但是数据还存在数据库中,方便数据恢复和有价值数据的保护。他还是删除,如果还需要重新查询出来,就不要使用逻辑删除可以通过状态字段控制,随时恢复比如说员工离职账号冻结了,有需要使用他账号来完成交接或者过度的时候还能激活。

8.2.1. 配置插件

在spring的配置文件中,配置com.baomidou.mybatisplus.extension.injector.LogicSqlInjector的bean,然后在MP的全局配置中引用即可,如果需要对逻辑值制定配置,在dbConfig里面增加对应的属性。

<!--MP的全局配置-->
<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
    <property name="dbConfig" ref="dbConfig"/>
    <property name="sqlInjector" ref="sqlInjector"></property>
</bean>
<!--数据库配置-->
<bean id="dbConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig">
    <!--<property name="tablePrefix" value="tbl_"/>-->
    <property name="idType"  value="AUTO"></property>
    <property name="logicDeleteValue" value="1"></property>
    <property name="logicNotDeleteValue" value="0"></property>
</bean>
<!--逻辑删除插件-->
<bean id="sqlInjector" class="com.baomidou.mybatisplus.extension.injector.LogicSqlInjector"/>

8.2.2. 实体类字段上加上@TableLogic注解

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class User implements Serializable {
  ...
    /**
     * 逻辑删除 0-未删除 1-删除
     */
    @TableLogic
    private Integer isDelete;
    ...
}

8.2.3. 测试代码

@Test
public void testLogicDeleteById() {
    userMapper.deleteById(1);
    userMapper.selectById(1);
    //要把逻辑删除的数据查询出来再次改为正常呢?
    List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getIsDelete, 1));
    //查询失败
    userList.forEach(System.out::println);
}

输出的sql,可以看到真正执行的是更新操作,将is_delete设置为1;

14  16:17:55.343 [main] DEBUG com.zzlh.mp.mapper.UserMapper.deleteById - ==>
UPDATE user SET is_delete=1
 WHERE id=1 AND is_delete=0;
------------------------------------------------------------------------------------------------------------------------
15  16:17:55.348 [main] DEBUG com.zzlh.mp.mapper.UserMapper.selectById - ==>
SELECT id,name,age,email,is_delete
 FROM user
 WHERE id=1 AND is_delete=0;
------------------------------------------------------------------------------------------------------------------------
SELECT id,name,age,email,is_delete
 FROM user
 WHERE is_delete=0 AND is_delete = 1;
------------------------------------------------------------------------------------------------------------------------

注:
- 使用mp自带方法删除和查找都会附带逻辑删除功能 (自己写的xml不会)
- 询问MP开发者后 开发者说明 因为大部分情况在删除之后不会恢复 所以没有设定相关恢复或者跳过 当前 筛选的 接口

8.3. 通用枚举

解决了繁琐的配置,让 mybatis 优雅的使用枚举属性!
主要是升级后的改变:
自3.1.0开始,可配置默认枚举处理类来省略扫描通用枚举配置 默认枚举配置

  • 升级说明:

    3.1.0 以下版本改变了原生默认行为,升级时请将默认枚举设置为EnumOrdinalTypeHandler

  • 影响用户:

    实体中使用原生枚举

  • 其他说明:

    配置枚举包扫描的时候能提前注册使用注解枚举的缓存

  • 推荐配置:

    • 使用实现IEnum接口
      • 推荐配置defaultEnumTypeHandler
    • 使用注解枚举处理
      • 推荐配置typeEnumsPackage
    • 注解枚举处理与IEnum接口
      • 推荐配置typeEnumsPackage
    • 与原生枚举混用
      • 需配置defaultEnumTypeHandler与 typeEnumsPackage

8.3.1. 申明通用枚举属性

方式一,枚举属性,实现 IEnum 接口

public enum AgeEnum implements IEnum<Integer> {
    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");

    private int value;
    private String desc;

    AgeEnum(final int value, final String desc) {
        this.value = value;
        this.desc = desc;
    }

    @Override
    public Integer getValue() {
        return value;
    }
}

方式二,原生的枚举类型,官网现在已经没有这个实例了

public enum GenderEnum {
    MALE,
    FEMALE;
}

方式三,使用 @EnumValue 注解枚举属性

public enum GradeEnum {
    PRIMARY(1, "小学"),
    SECONDORY(2, "中学"),
    HIGH(3, "高中");

    GradeEnum(int code, String descp) {
        this.code = code;
        this.descp = descp;
    }

    @EnumValue//标记数据库存的值是code
    private final int code;
    private final String descp;

    public int getCode() {
        return code;
    }

    public String getDescp() {
        return descp;
    }
}

实体属性使用枚举类型,只展示使用枚举的属性。

public class User {
    /**
     * 年龄
     */
    private AgeEnum age;

    /**
     * 性别,0:MALE, 1:FEMALE
     */
    private GenderEnum gender;

    /**
     * 年级
     */
    private GradeEnum grade;
}

8.3.2. 配置扫描通用枚举

<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <property name="configuration" ref="configuration"></property>
    <!--注解枚举处理与IEnum接口配置typeEnumsPackage-->
    <property name="typeEnumsPackage" value="com.zzlh.mp.enums"/>
</bean>
<!--与原生枚举混用,配置defaultEnumTypeHandler-->
<bean id="configuration" class="com.baomidou.mybatisplus.core.MybatisConfiguration">
    <property name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
</bean>

8.3.3. 编写测试代码

@Test
public void insert() {
    User user = new User();
    user.setName("K神");
    user.setAge(AgeEnum.ONE);
    user.setGrade(GradeEnum.HIGH);
    user.setGender(GenderEnum.MALE);
    user.setEmail("abc@mp.com");
    System.out.println("result:"+userMapper.insert(user));
    // 成功直接拿会写的 ID
    System.err.println("\n插入成功 ID 为:" + user.getId());

    List<User> list = userMapper.selectList(null);
    for(User u:list){
        System.out.println(u);
        System.out.println(u.getAge());
        if(u.getId().equals(user.getId())){
            System.out.println(u.getGender());
            System.out.println(u.getGrade());

        }
    }
}

@Test
public void delete() {
    System.out.println("result:"+userMapper.delete(new QueryWrapper<User>()
            .lambda().eq(User::getAge, AgeEnum.TWO)));
}

@Test
public void update() {
    System.out.println("result:"+userMapper.update(new User().setAge(AgeEnum.TWO),
            new QueryWrapper<User>().eq("age", AgeEnum.THREE)));
}

@Test
public void select() {
    User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getId, 2));
    System.out.println("name:"+user.getName());
    System.out.println("age:"+user.getAge());
}

对应的SQL和输出

insert

INSERT INTO user ( id, name, age, gender, grade, email ) VALUES ( 1152049395573956610, 'K神', 1, 0, 3, 'abc@mp.com' );
------------------------------------------------------------------------------------------------------------------------

插入成功 ID 为:1152049395573956610

SELECT id,name,age,gender,grade,email FROM user
------------------------------------------------------------------------------------------------------------------------
User(id=2, name=张三, age=null, gender=FEMALE, grade=SECONDORY, email=zs@mp.com)
null
User(id=1152049395573956610, name=K神, age=ONE, gender=MALE, grade=HIGH, email=abc@mp.com)
ONE
MALE
HIGH

delete

DELETE FROM user WHERE age = 2
------------------------------------------------------------------------------------------------------------------------

update

UPDATE user SET age=2 WHERE age = 3
------------------------------------------------------------------------------------------------------------------------

select

SELECT id,name,age,gender,grade,email FROM user WHERE id = 2;

name:张三
age:null
------------------------------------------------------------------------------------------------------------------------

8.3.4. JSON序列化处理

如果需要对使用了通用枚举的对象进行json序列化转化,比如restful风格的,可以使用JSON序列化处理来帮住枚举的转换。
官方提供了两种工具包的转换方案:

8.3.4.1. Jackson

在需要响应描述字段的get方法上添加@JsonValue注解即可,官方文档上说还需要在枚举中复写toString方法经过试验这个包中不需要复写即可。

public enum GradeEnum {
    PRIMARY(1, "小学"),
    SECONDORY(2, "中学"),
    HIGH(3, "高中");

    GradeEnum(int code, String descp) {
        this.code = code;
        this.descp = descp;
    }

    @EnumValue
    private final int code;
    private final String descp;

    public int getCode() {
        return code;
    }
    @JsonValue
    public String getDescp() {
        return descp;
    }

}

编写测试类

@Test
public void select() throws JsonProcessingException {
    User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getId, 2));
    System.out.println(JSON.toJSONString(user));
    //{"age":"ONE","email":"zs@mp.com","gender":"FEMALE","grade":"SECONDORY","id":2,"name":"张三"}
    System.out.println(new ObjectMapper().writeValueAsString(user));
    //{"id":2,"name":"张三","age":"一岁","gender":"FEMALE","grade":"中学","email":"zs@mp.com"}
}

8.3.4.2. Fastjson

Fastjson在实验中并没有实现,去找官方提供的实例中,并没有关于JSON序列化处理这一块的例子,只是演示了使用通用枚举属性相关的东西。然后我联系了官方的人员,希望能得到这一块的相关的使用说明,结果被回复了五个字自己去百度

image-20210113170411206
我自己找Fastjson相关资料的时候,看了注解中SerializerFeature.WriteEnumUsingToString这一点,这个是Fastjson提供的关于枚举类型序列化的时候处理方案的一种,我看了里面有好几种,那么我猜测这些序列化的内容应该是Fastjson包自己的东西而不是MP的特性?这个有待不使用MP的时候再验证。
我又去找官方网站上介绍的优秀案例上看看能不能找到相关的例子,只在Crown这个项目里找到使用枚举的情况,但是项目中使用的是Jackson这种方式。

image-20210113170432296

8.4. 乐观锁插件

悲观锁:认为每次对数据的操作的时候,数据都是有风险的、不安全的、随时随地都会被别人。所以当开始一个修改数据操作的时候。该条数据被锁定,直到本次修改完成之后,再释放锁。

image-20210113170450721

image-20210113170514591
乐观锁:当要更新一条记录的时候,希望这条记录没有被别人更新。

特别说明:

支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1,newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用!!!

乐观锁实现方式:
取出记录时,获取当前version

更新时,带上这个version

执行更新时, set version = newVersion where version = oldVersion

如果version不对,就更新失败

8.4.1. 配置乐观锁插件

在spring的配置文件里,将乐观锁插件com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor配置到sqlSessionFactory里面。

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
    <property name="configuration" ref="configuration"></property>
    <!--加入MP的全局配置-->
    <property name="globalConfig" ref="globalConfig"></property>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    <property name="typeEnumsPackage" value="com.zzlh.mp.enums"/>
    <!-- 配置插件 -->
    <property name="plugins">
        <list>
            <!--分页插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor"/>
            <!--乐观锁插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor"/>
        </list>
    </property>
</bean>

8.4.2. 添加乐观锁版本字段注解

数据库表增加version之后,在生成的实体中version属性上加上@Version注解即可。

@Data
public class User{
	......
    /**
     * 版本
     */
    @Version
    private Integer version;
}

8.4.3. 编写测试类

@Test
public void testUpdateByIdSucc() {
    User user = new User();
    user.setAge(18);
    user.setEmail("test@baomidou.com");
    user.setName("optlocker");
    user.setVersion(1);
    userMapper.insert(user);
    Long id = user.getId();

    User userUpdate = new User();
    userUpdate.setId(id);
    userUpdate.setAge(19);
    userUpdate.setVersion(1);
    Assert.assertEquals("Should update success", 1, userMapper.updateById(userUpdate));
    Assert.assertEquals("Should version = version+1", 2, userUpdate.getVersion().intValue());
}

@Test
public void testUpdateByIdFail() {
    User user = new User();
    user.setAge(18);
    user.setEmail("test@baomidou.com");
    user.setName("optlocker");
    user.setVersion(1);
    userMapper.insert(user);
    Long id = user.getId();

    User userUpdate = new User();
    userUpdate.setId(id);
    userUpdate.setAge(19);
    userUpdate.setVersion(0);
    Assert.assertEquals("Should update failed due to incorrect version(actually 1, but 0 passed in)", 0, userMapper.updateById(userUpdate));
}

@Test
public void testUpdateByIdSuccWithNoVersion() {
    User user = new User();
    user.setAge(18);
    user.setEmail("test@baomidou.com");
    user.setName("optlocker");
    user.setVersion(1);
    userMapper.insert(user);
    Long id = user.getId();

    User userUpdate = new User();
    userUpdate.setId(id);
    userUpdate.setAge(19);
    userUpdate.setVersion(null);
    Assert.assertEquals("Should update success as no version passed in", 1, userMapper.updateById(userUpdate));
    User updated = userMapper.selectById(id);
    Assert.assertEquals("Version not changed", 1, updated.getVersion().intValue());
    Assert.assertEquals("Age updated", 19, updated.getAge().intValue());
}

/**
 * 批量更新带乐观锁
 * <p>
 * update(et,ew) et:必须带上version的值才会触发乐观锁
 */
@Test
public void testUpdateByEntitySucc() {
    QueryWrapper<User> ew = new QueryWrapper<>();
    ew.eq("version", 1);
    int count = userMapper.selectCount(ew);

    User entity = new User();
    entity.setAge(28);
    entity.setVersion(1);

    Assert.assertEquals("updated records should be same", count, userMapper.update(entity, null));
    ew = new QueryWrapper<>();
    ew.eq("version", 1);
    Assert.assertEquals("No records found with version=1", 0, userMapper.selectCount(ew).intValue());
    ew = new QueryWrapper<>();
    ew.eq("version", 2);
    Assert.assertEquals("All records with version=1 should be updated to version=2", count, userMapper.selectCount(ew).intValue());
}

对应的SQL和输出
testUpdateByIdSucc

INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19, version=2
 WHERE id=18 AND version=1;
------------------------------------------------------------------------------------------------------------------------

testUpdateByIdFail

INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19, version=3
 WHERE id=19 AND version=2;
------------------------------------------------------------------------------------------------------------------------

testUpdateByIdSuccWithNoVersion

INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19
 WHERE id=20;
------------------------------------------------------------------------------------------------------------------------
SELECT id,name,age,email,version
 FROM user
 WHERE id=20;
------------------------------------------------------------------------------------------------------------------------

testUpdateByEntitySucc

SELECT COUNT( 1 )
 FROM user
 WHERE version = 1;
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=28, version=2
 WHERE version = 1;
------------------------------------------------------------------------------------------------------------------------
SELECT COUNT( 1 )
 FROM user
 WHERE version = 1;
------------------------------------------------------------------------------------------------------------------------
SELECT COUNT( 1 )
 FROM user
 WHERE version = 2;
------------------------------------------------------------------------------------------------------------------------

8.5. 执行 SQL 分析打印

该功能依赖 p6spy 组件,完美的输出打印 SQL 及执行时长 3.1.0 以上版本。也就是说我们不需要使用idea的插件,就能在控制台输出sql语句。

8.5.1. 引入p6spy包

<dependency>
  <groupId>p6spy</groupId>
  <artifactId>p6spy</artifactId>
  <version>3.8.2</version>
</dependency>

8.5.2. 配置p6spy连接设置

在spring配置文件里面,对原来的dataSource进行修改,外面包一层p6spy的配置。

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <property name="dataSource" ref="dataSources"></property>
</bean>
<!--p6spy连接设置-->
<bean id="dataSources" class="com.p6spy.engine.spy.P6DataSource">
    <constructor-arg ref="dataSource"/>
</bean>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="maxActive" value="10" />
    <property name="minIdle" value="5" />
    <property name="logAbandoned" value="true" />
</bean>  

然后在resources的根目录下,增加一个spy.properties配置文件,类似于log4j的控制台打印设置。

module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

注:该插件有性能损耗,不建议生产环境使用。

8.5.3. 控制台打印输出

控制台就会打印出真实的sql语句和执行时间了。

image-20210113170542111

8.6. 性能分析插件

跟执行SQL分析打印比较类似,性能分析拦截器,用于输出每条 SQL 语句及其执行时间。

8.6.1. 配置性能分析插件

在spring配置文件的sqlSessionFactory里面配置插件,com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor。

<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <!-- 配置插件 -->
    <property name="plugins">
        <list>
            <!--性能分析插件-->
            <bean class="com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor">
                <!--sql 最大执行时长 -->
                <property name="maxTime" value="10" />
                <!--SQL是否格式化 默认false-->
                <property name="format" value="true" />
            </bean>
        </list>
    </property>
</bean>

可以看到改插件有两个属性,format是配置sql是否格式化的,而maxTime是配置sql执行最大时长,如果sql语句执行时长超过配置的时间就会报异常,有助于测试的时候排查sql语句的性能。
注:此插件只用于开发环境或测试环境,不建议生产环境使用。

8.6.2. 控制台打印输出

image-20210113170609844

8.7. Sql 注入器

这里的sql注入器和sql注入攻击没有关系,是指除了通用CRUD提供的17中方法之外,通过sql注入器将自定义的方法注入到MyBatis容器中,只要继承BaseMapper就能使用的功能插件。
在讲解MP加载自动注入的时候,说过通用的CRUD操作是AbstractSqlInjector类的inspectInject方法来实现注入的。AbstractSqlInjector是实现了ISqlInjector 接口,而自定义的通用方法类实现ISqlInjector的inspectInject方法,然后配置到spring里面,则也可以实现将自定义的方法注入到Mybatis的容器中。

8.7.1. 编写自定义的通用方法

继承AbstractMethod类,复写injectMappedStatement方法,使用addDeleteMappedStatement将sqlSource和method生成一个新的MappedStatement。

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);
    }
}

8.7.2. 编写自定义Sql注入器

继承DefaultSqlInjector,复写getMethodList方法。将自定的通用方法加入到方法列表中,也就是说除了默认的17个方法之外,增加一个新的自定义方法。

public class MySqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new DeleteAll());
        return methodList;
    }
}

8.7.3. 将自定义的SQL注入器配置到spring里

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <!--加入MP的全局配置-->
    <property name="globalConfig" ref="globalConfig"></property>
</bean>

<!--MP的全局配置-->
<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
    <property name="sqlInjector" ref="sqlInjector"></property>
</bean>

<!--自定义sql注入器-->
<bean id="sqlInjector" class="com.zzlh.mp.MySqlInjector"/>

8.7.4. 编写测试代码

Mapper.java文件继承BaseMapper之后,将自定义的方法写好,不用写SQL语句将会自动注入。

public interface StudentMapper extends BaseMapper<Student> {
    void deleteAll();
}

测试类里面就可以直接使用deleteAll方法。

@Autowired
private UserMapper userMapper;

@Test
public void test(){
    userMapper.deleteAll();
}

可以看一下控制台的输出,是不是使用了我们自定义的sql语句。

 Time:19 ms - ID:com.zzlh.mp.mapper.UserMapper.deleteAll
Execute SQL:
    delete 
    from
        USER

8.8. 自动填充功能

这个插件是在基础使用场景中,最后一个插件,也是很重要的、使用很频繁的插件。
自动填充功能类似于数据库字段的默认值,也就是说当开启了这个功能,并指定字段使用自动填充功能,在没有给实例的字段设置值的时候,就会自动把设置好的值填充到实例的字段里,最终在生成的SQL语句里也会拼接上设置值,持久化到数据库。但是它比数据库的默认值优势在于,第一、可以指定场景,是插入的时候设置默认值还是更新的时候设置默认值,或者两种情况都设置。第二、自动填充的也可以是通过方法计算的可变化的值。

那么,根据该功能的情况,我们结合之前学习到的主键策略类型中的INPUT类型,将策略配置为INPUT,然后配置主键字段自动填充,从而使用我们自定义的主键生成规则生成主键。或者如果有这样的需求,每个数据表中都有创建时间和最后更新时间的字段用来标记数据的时间,那么通过配置插入和更新的自动填充,就可以实现该业务需求了。

8.8.1. 编写自定义的填充器

这里我把内控中使用的UUIDUtils移植过来,来填充ID。

public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        System.out.println("start insert fill ....");
        //避免使用metaObject.setValue()
        this.setFieldValByName("id", UUIDUtils.getUUID(), metaObject);
        this.setFieldValByName("createtime", new Date(), metaObject);
        this.setFieldValByName("modifytime", new Date(), metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        System.out.println("start update fill ....");
        this.setFieldValByName("modifytime", new Date(), metaObject);
    }
}

注:
- 必须使用父类的setFieldValByName()或者setInsertFieldValByName/setUpdateFieldValByName方法,否则不会根据注解FieldFill.xxx来区分。
- 填充属性的字段不再是数据表中的字段名,而是实体中的属性名。因为自动填充是自动将值填充到了实例中,再由MP转换为SQL语句。

8.8.2. 将自定义的填充器配置到spring里

自动注入器需要配置到全局配置中,如图所示。

<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
    <!--加入MP的全局配置-->
    <property name="globalConfig" ref="globalConfig"></property>
</bean>

<!--MP的全局配置-->
<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
    <property name="dbConfig" ref="dbConfig"/>
    <property name="metaObjectHandler" ref="myMetaObjectHandler"/>
</bean>
<!--数据库配置-->
<bean id="dbConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig">
    <property name="idType"  value="INPUT"/>
</bean>

<!--自动填充器-->
<bean id="myMetaObjectHandler" class="com.zzlh.mp.handler.MyMetaObjectHandler"/>

8.8.3. 编写测试代码

@Test
public void test(){
    //先新增一条数据,ID为null,不设置创建时间和更新时间
    User user = new User(null,"Tom",1,"tom@qq.com",null);
    userMapper.insert(user);

    //将新增的数据查询出来
    User updateUser = userMapper.selectById(user.getId());
    //不做任何修改更新数据
    userMapper.updateById(updateUser);
}

控制台输出的SQL语句为:

INSERT INTO USER ( ID, NAME, AGE, EMAIL, OPERATOR, create_time, modify_time ) VALUES ( '201907251004021911432201111', '张三', 20, '22@qq.com', '管理员', '2019-07-25 10:04:02.191', '2019-07-25 10:04:02.191' );
------------------------------------------------------------------------------------------------------------------------
SELECT ID,NAME,AGE,EMAIL,OPERATOR,create_time,modify_time
 FROM USER
 WHERE ID='201907251004021911432201111';
------------------------------------------------------------------------------------------------------------------------
UPDATE USER SET NAME='张三', AGE=20, EMAIL='22@qq.com', OPERATOR='管理员', create_time='2019-07-25 10:04:02.0', modify_time='2019-07-25 10:04:50.718'
 WHERE ID='201907251004021911432201111';
------------------------------------------------------------------------------------------------------------------------

8.9. 动态数据源

8.10. 分布式事务

8.11. 多租户 SQL 解析器

需要分页拦截器 使用,主要过滤查询数据。

8.12. 动态表名 SQL 解析器

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消