MybatisPlus功能使用

版本与基础依赖

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.5.3.1</version>
</dependency>
        
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
	<version>3.5.3.1</version>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.21</version>
</dependency>	

MyBatisX插件安装使用

在File->Settings->Plugins下载插件

image-20230912134730167

点击右侧DataBase选择对应数据库类型

image-20230912135129183

按照数据库信息填写相关连接参数,TestConnection测试连接

image-20230912135500258

点击 0 of 13

image-20230912140041465

勾选数据库

image-20230912140133899

选择对应的表,右键生成

image-20230912140337015

image-20230912143017732

image-20230912141127936

自定义语句

除了使用默认生成的BaseMapper之外,也可以向Mybatis一样在xml添加自定义sql

Mapper.xml

mp默认生成是空的,可以调用BaseMapper来使用默认sql操作。

这里我们添加自定义查询语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.dao.PertwoMapper">

   <!-- <resultMap id="BaseResultMap" type="com.baomidou.entity.Pertwo">
            <id property="code" column="code" jdbcType="VARCHAR"/>
            <result property="deleted" column="deleted" jdbcType="INTEGER"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="sex" column="sex" jdbcType="VARCHAR"/>
            <result property="job" column="job" jdbcType="VARCHAR"/>
    </resultMap> -->

    <sql id="Base_Column_List">
       select  code,deleted,name,
        age,sex,job from pertwo
    </sql>


    <select id="selectCode" parameterType="String" resultType="Pertwo">
        <include refid="Base_Column_List"/>
        where code = #{code}
    </select>

</mapper>

Mapper接口

在mapper.java中添加xml对应的方法

import com.baomidou.entity.Pertwo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Component;

//注意将泛型换成实体类
public interface PertwoMapper extends BaseMapper<Pertwo> {
	//自定义语句
    Pertwo selectCode(String code);
}

业务接口

如果只用mapper的java接口进行调用,不需要用service,此处可以不写,就用默认生成的就行

import com.baomidou.entity.Pertwo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface PertwoService extends IService<Pertwo> {

    Pertwo selectCode(String code);

}

业务接口实现类

如果只用mapper的java接口进行调用,不需要用service,此处可以不写,就用默认生成的就行

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.entity.Pertwo;
import com.baomidou.service.PertwoService;
import com.baomidou.dao.PertwoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PertwoServiceImpl extends ServiceImpl<PertwoMapper, Pertwo>
    implements PertwoService{

    @Autowired
    private PertwoMapper pw;

    @Override
    public Pertwo selectCode(String code) {

        return pw.selectCode(code);
    }
}

测试调用

@RestController
@RequestMapping("/user")
public class UserController {
	
	@Autowired
    private PertwoServiceImpl pertwoServiceImpl;
	
	@RequestMapping("/qpt")
    public Pertwo qst(@RequestParam String code){

        Pertwo p = pertwoServiceImpl.selectCode(code);
        return p;
    }
}

主键生成策略详解

NONE(无状态)

如果使用 IdType.NONE 策略,表示未设置主键类型,会使用默认雪花算法,如果手动赋值,则为手动赋予的值

@TableId(type=IdType.NONE)

@TableName(value ="person")
public class Person implements Serializable {
    /**
     * 
     */
    @TableId(type=IdType.NONE)
    private String id;

    /**
     * 
     */
    private String name;

    /**
     * 
     */
    private Integer age;
}

service新增

    @RequestMapping("/ins")
    public String insUser(){
        User user = new User();
        //p.setId("lyh"); //如果此处放开,则id为lyh
        user.setAge(2);
        user.setName("阿比西尼亚");
        user.setEmail("12222@qq.com");
        iUserService.save(user);
        return "success";
    }

结果

自动:

image-20230831143812519

手动设置:

image-20230902091428007

ASSIGN_UUID(不含中划线的UUID)

@TableId(type = IdType.ASSIGN_UUID)

@TableName(value ="person")
public class Person implements Serializable {
    /**
     * 
     */
    @TableId(type = IdType.ASSIGN_UUID)
    private String code;

    /**
     * 
     */
    private String name;

    /**
     * 
     */
    private Integer age;

    /**
     * 
     */
    private String sex;

    /**
     * 
     */
    private String job;
}

service新增

如果手动设置,则结果为手动值,与none的一样

	@RequestMapping("/inp")
    public String insPer(){
        Person p = new Person();
        //p.setCode("lyh");//此处放开则code为lyh
        p.setSex("女");
        p.setName("布偶");
        p.setAge(3);
        p.setJob("pet");
        personService.save(p);
        return "success";
    }

结果

image-20230831145346267

ASSIGN_ID(雪花算法)

@TableId(type = IdType.ASSIGN_ID)

@TableName(value ="person")
public class Person implements Serializable {
    /**
     * 
     */
    @TableId(type = IdType.ASSIGN_ID)
    private String code;

    /**
     * 
     */
    private String name;

    /**
     * 
     */
    private Integer age;

    /**
     * 
     */
    private String sex;

    /**
     * 
     */
    private String job;
}

service新增

如果手动设置,则结果为手动值,与none的一样

@RequestMapping("/inp")
public String insPer(){
	Person p = new Person();
    //p.setCode("lyh");//此处放开则code为lyh
	p.setSex("男");
	p.setName("bigworth");
	p.setAge(25);
	p.setJob("boss");
	personService.save(p);
	return "success";
}

结果

image-20230831145610536

Input(自定义输入策略)

@TableId(type = IdType.INPUT)

@TableName(value ="person")
public class Person implements Serializable {
    /**
     * 
     */
    @TableId(type = IdType.INPUT)
    private String code;

    /**
     * 
     */
    private String name;

    /**
     * 
     */
    private Integer age;

    /**
     * 
     */
    private String sex;

    /**
     * 
     */
    private String job;
}

service新增

    @RequestMapping("/inp")
    public String insPer(){
        Person p = new Person();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        p.setAge(18);
        p.setJob("boss");
        personService.save(p);
        return "success";
    }

结果

image-20230831153244270

AUTO(自动增长策略)

@TableId(type = IdType.AUTO)

需要设置主键自增且为数字类型

image-20230902090520070

public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    private String email;
}

service新增

	@RequestMapping("/ins")
    public String insUser(){
        User user = new User();
        user.setAge(2);
        user.setName("豹猫");
        user.setEmail("66666@qq.com");
        iUserService.save(user);
        return "success";
    }

结果

从21到23主键增加

image-20230902090702436

注意

ASSIGN_IDASSIGN_UUIDNONE三种状态,如果代码里手动设置了主键值,则以手动设置为主

逻辑删除

添加一个字段用作删除标记,比如1为已删除,0为未删除

  • 执行删除操作时,实际是更新,将标记字段变为1,而不是真正的删除
  • 执行查询操作时,查标记为0的数据
  • 执行更新时,更新标记为0的数据
  • 支持所有数据类型(推荐使用 Integer,Boolean,LocalDateTime)

配置

建表,注意新增数据操作,mp的逻辑删除不会对标记字段做额外操作,需要自己设置默认值

drop table pertwo;
CREATE TABLE `pertwo` (
  `deleted` int DEFAULT 0 COMMENT '是否被删除 0 未删除 1 已删除', --设置默认值为0,代表未删除
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `code` varchar(255) NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

yaml

#mybatis plus是独立节点,需要单独配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 全局逻辑删除的实体字段名,也可实体类字段上加上@TableLogic注解
      logic-not-delete-value: 0  # 逻辑未删除值
      logic-delete-value: 1  # 逻辑已删除值

代码测试

先新增一条数据

	@RequestMapping("/inpt")
    public String inst(){
        Pertwo p = new Pertwo();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        p.setAge(18);
        p.setJob("boss");
        pertwoServiceImpl.save(p);
        return "success";
    }

执行结果,因为建表时设置删除标记字段默认值为0.新增后deleted值为0

image-20230902104138264

再执行更新操作,saveOrUpdate方法按主键进行更新

	@RequestMapping("/uppt")
    public String upst(){
        Pertwo p = new Pertwo();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        p.setAge(18);
        p.setJob("super boss");
        pertwoServiceImpl.saveOrUpdate(p);
        return "success";
    }

操作结果,看见语句日志自动多了AND deleted=0代表只更新未删除数据

image-20230902104434948

image-20230902103016062

再执行删除操作

利用Wrapper构造条件

	@RequestMapping("/dept")
    public String upst(){
        pertwoServiceImpl.remove(new QueryWrapper<Pertwo>().eq("code","lyh"));
        return "success";
    }

操作结果,看见语句日志自动多了AND deleted=0,并且删除改为update更新

image-20230902105400409

最后再查询

	@RequestMapping("/qupt")
    public Pertwo qust(){
        Pertwo p = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("code","lyh"));
        return p;
    }

结果因为上面进行逻辑删除了,deleted=1,所以查不到

image-20230902105839009

通用枚举

直接在代码中使用枚举常量,数据库自动变为其对应的枚举值

配置

定义枚举类

3.5.2 版本以前:

需要重写getValue()方法来获取值,@JsonValue用于前端正常返回json格式数据

import com.baomidou.mybatisplus.core.enums.IEnum;
import com.fasterxml.jackson.annotation.JsonValue;

public enum AgeEnum implements IEnum<Integer> {

    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");

    private int value;
    private String desc;

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


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

    @JsonValue
    public String getDesc(){
        return this.desc;
    }

}

从 3.5.2 版本开始:

不需要实现IEnum接口;

  • @EnumValue加在数值字段上,也就是需要存入数据库的字段
  • @JsonValue加在文字字段上,也就是用于展示,以及需要返回前端的json格式数据
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

public enum AgeEnum
{

    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");


    /**
     * 数据库存放数值,前端展示汉字
     * */
    @JsonValue
    private String desc;
    @EnumValue
    private int value;

    
    /**
     * get、set方法及构造还要有
     * */
    AgeEnum(int i, String age) {
        this.value = i;
        this.desc = age;
    }
	   
    public Integer getValue() {
        return this.value;
    }

    public String getDesc(){
        return this.desc;
    }

}

实体类添加枚举属性

@TableName(value ="pertwo")
public class Pertwo implements Serializable {

    @TableId
    private String code;

    private String name;
    
    //省略其他属性
    .........

    /**
     * 枚举属性,包括get与set方法
     */
    private AgeEnum age;
    
    public AgeEnum getAge() {
        return age;
    }
    public void setAge(AgeEnum age) {
        this.age = age;
    }
}

yaml配置

3.5.2 版本开始不需要配这个

配置枚举类包扫描

#mybatis plus是独立节点,需要单独配置
mybatis-plus:
  #扫描枚举类所在包
  type-enums-package: com.baomidou.Enum  # 支持统配符 * 或者 ; 分割

代码测试

新增

@RequestMapping("/inpt")
public String inst(){
        Pertwo p = new Pertwo();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        p.setJob("boss");
        p.setAge(AgeEnum.THREE);//直接赋值枚举
        pertwoServiceImpl.save(p);
        return "success";
}

因为重写了getValue()方法,数据库显示为枚举的数字value

image-20230904111058717

@JsonValue前端显示

@RequestMapping("/qupt")
public Pertwo qust(){
    
        Pertwo p = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("code","lyh"));
        return p;
    
}

页面查询结果

image-20230904111159983

如果不用@JsonValue,显示为:

image-20230904112829061

使用枚举作为条件构造查询

@RequestMapping("/mupt")
public Pertwo must(){

        Pertwo p = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("age",AgeEnum.THREE));
        return p;
        
}    

页面显示

image-20230904111246158

字段类型处理器

类型处理器,用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值

配置类型转换

实体类@TableName(autoResultMap = true)@TableField(typeHandler = *.class)必须加

//实体类Entity
@TableName(autoResultMap = true)
public class Pertwo implements Serializable {
	
	/**
     *  要进行json类型转换的字段:job
     *  转换器:FastjsonTypeHandler
     */
    @TableField(typeHandler = FastjsonTypeHandler.class)
    private Map<String,String> job;

    public Map<String,String> getJob() {
        return job;
    }

    public void setJob(Map<String,String> job) {
        this.job = job;
    }
    
}

如果mapper.xml有resultMap且类型不对要去掉,或者添加typeHandler标签

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.dao.PertwoMapper">

   	<!-- 去掉整个resultMap,这里注释掉,或者用下面那个为job字段添加了typeHandler的resultMap -->
	<!-- <resultMap id="BaseResultMap" type="com.baomidou.entity.Pertwo">
            <id property="code" column="code" jdbcType="VARCHAR"/>
            <result property="deleted" column="deleted" jdbcType="INTEGER"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="sex" column="sex" jdbcType="VARCHAR"/>
            <result property="job" column="job" jdbcType="VARCHAR"/>
    </resultMap>-->
	
    <!-- 或者用这个 -->
    <resultMap id="BaseResultMap" type="com.baomidou.entity.Pertwo">
            <id property="code" column="code" jdbcType="VARCHAR"/>
            <result property="deleted" column="deleted" jdbcType="INTEGER"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="sex" column="sex" jdbcType="VARCHAR"/>
            <result property="job" column="job" jdbcType="VARCHAR"
                    typeHandler="com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler"/>
    </resultMap>
    
    <sql id="Base_Column_List">
        code,deleted,name,
        age,sex,job
    </sql>
</mapper>

测试代码

@RequestMapping("/inpt")
public String inst(){

        Pertwo p = new Pertwo();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        
        //map变为json字符串存入数据库
        Map<String, String> contact = new HashMap<>();
        contact.put("phone", "010-1234567");
        contact.put("tel", "13388889999");
        contact.put("job", "boss");
        p.setJob(contact);
        
        p.setAge(AgeEnum.THREE);
        pertwoServiceImpl.save(p);
        return "success";
        
}

执行结果

image-20230904151813744

自定义类型处理器

TypeHandler

自定义类型处理器需要实现TypeHandler

image-20230904154216674

TypeHandler拥有很多实现

image-20230904154310182

代码

这里实现一个数据库vachar字符串转实体属性List

定义转换器

import lombok.extern.java.Log;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

@MappedJdbcTypes(JdbcType.VARCHAR)  //数据库类型
@MappedTypes({List.class})          //java数据类型
@Log
public class ListTypeHandler implements TypeHandler<List<String>> {


    @Override
    public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType)
            throws SQLException {

        String hobbys = dealListToOneStr(parameter);
        ps.setString(i , hobbys);
    }


    /**
     * 集合拼接字符串
     * @param parameter
     * @return
     */
    private String dealListToOneStr(List<String> parameter){

        if(parameter == null || parameter.size() <=0) {
            return null;
        }

        String res = "";

        for (int i = 0 ;i < parameter.size(); i++) {
            if(i == parameter.size()-1){
                res+=parameter.get(i);
                return res;
            }
            res+=parameter.get(i)+",";
        }
        return null;

    }

     

    @Override
    public List<String> getResult(ResultSet rs, String columnName)

            throws SQLException {

        log.info("method ====>>> getResult(ResultSet rs, String columnName)");

        return Arrays.asList(rs.getString(columnName).split(","));
    }

 

    @Override
    public List<String> getResult(ResultSet rs, int columnIndex)

            throws SQLException {

        log.info("method ====>>> getResult(ResultSet rs, int columnIndex)");

        return Arrays.asList(rs.getString(columnIndex).split(","));

    }

    @Override
    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException{

        log.info("method ====>>> getResult(CallableStatement cs, int columnIndex)");

        String hobbys = cs.getString(columnIndex);

        return Arrays.asList(hobbys.split(","));

    }

}

实体类添加typeHandler

将上面的typeHandler实现加入实体类注解

//实体类Entity
@TableName(autoResultMap = true)
public class Pertwo implements Serializable {
	
	/**
     *  转换器:ListTypeHandler
     */
    @TableField(typeHandler = ListTypeHandler.class)
    private Map<String,String> job;

    public Map<String,String> getJob() {
        return job;
    }

    public void setJob(Map<String,String> job) {
        this.job = job;
    }
    
}

注意,如果mapper.xml没有使用默认的xml(MybatisPlus默认生成的xml里面是空的),而是自己添加了自定义语句,则需要进行映射配置,否则查询结果该字段为null。

如下,实体类job属性是List:

image-20230906150423402

数据库中的job字段值为:

image-20230906150600068

mapper.xml中添加了自定义查询id=“selectCode”这种是无法使用中的标签进行拼接的,需要在结果映射中添加类型转换:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.dao.PertwoMapper">

    <sql id="Base_Column_List">
       select  code,deleted,name,
        age,sex,job from pertwo
    </sql>

	<!-- 这是自己添加的自定义查询 -->
    <select id="selectCode" parameterType="String" resultMap="BaseResultMap">
        <include refid="Base_Column_List"/>
        where code = #{code}
    </select>
    
    <!-- 结果集映射 -->
    <resultMap id="BaseResultMap" type="com.baomidou.entity.Pertwo">
            <id property="code" column="code" jdbcType="VARCHAR"/>
            <result property="deleted" column="deleted" jdbcType="INTEGER"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="sex" column="sex" jdbcType="VARCHAR"/>
             <!-- 添加类型转换 -->
        	<result property="job" column="job" jdbcType="VARCHAR" 
                    typeHandler="com.baomidou.typeHandler.ListTypeHandler"/>
    </resultMap>

</mapper>

测试

执行新增操作

@RequestMapping("/inpt")
public String inst(){
        Pertwo p = new Pertwo();
        p.setCode("lyh");
        p.setSex("女");
        p.setName("gril");
        //list变为varchar字符串存入数据库
        List<String> contact = new ArrayList<>();
        contact.add("一");
        contact.add("个");
        contact.add("boss");
        p.setJob(contact);

        p.setAge(AgeEnum.THREE);
        pertwoServiceImpl.save(p);
        return "success";
}

数据库显示:

image-20230904154034876

前端页面显示结果:

{"code":"lyh","deleted":0,"name":"gril","age":"三岁","sex":"女","job":["一","个","boss"]}

新增后执行查询操作

	//  mp自带默认的查询方法
    @RequestMapping("/mupt")
    public Pertwo must(){
        //此处使用了上面的枚举
        Pertwo p = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("age",AgeEnum.THREE));
        return p;
    }

    //  使用xml中自定义的select查询方法
    @RequestMapping("/qpt")
    public Pertwo qst(@RequestParam String code){

        Pertwo p = pertwoServiceImpl.selectCode(code);
        return p;
    }

结果:

{"code":"lyh","deleted":0,"name":"gril","age":"三岁","sex":"女","job":["一","个","boss"]}

自动填充

在实际操作过程中,我们并不希望创建时间、修改时间这些来手动进行,而是希望通过自动化来完成,而mybatis-plus则也提供了自动填充功能来实现这一操作

配置

注解

@TableField(fill= FieldFill.*)

默认值是:FieldFill.DEFAULT

描述
DEFAULT默认不处理
INSERT插入时填充字段
UPDATE更新时填充字段
INSERT_UPDATE插入和更新时填充字段

image-20230905142517517

代码

实体类注解添加 @TableField

@TableName(value ="work_publish")
public class WorkPublish implements Serializable {
	.........
	
	@TableField(fill= FieldFill.INSERT_UPDATE)
    private Date time;
    
    ..........
}

配置类

FieldFill.INSERT_UPDATE即新增修改都要配置

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * @Component 表示就是要把处理器 丢到IOC容器中  这一点千万不能忘记 !
 */
@Component
@Log
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入时候的填充策略
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill");
      
        //注解标记在了time字段上,这里就要给time做处理,为其自动填充当前时间
        this.setFieldValByName("time",new Date(),metaObject);
        
        //可以写很多字段的自动填充
        //this.setFieldValByName("createTime",new Date(),metaObject);
        //this.setFieldValByName("updateTime",new Date(),metaObject);
    }

    /**
     * 更新时候的填充策略,与上面类似
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill");
        this.setFieldValByName("time",new Date(),metaObject);

    }
}

测试

保存实体类时不手动保存时间字段

	@RequestMapping("/work2")
    public String mp2(){
        WorkPublish wp = new WorkPublish();
        wp.setId("A11112");
        wp.setType("2");
        wp.setPhone("13230707234");
        wp.setDescribe1("填充测试");
        wp.setPhoto("/profile/upload/2023/05/02/d434b20437c17ac316f9c29e8e839e4_20230502113157A001.png");
        wp.setSeatmap("/profile/baiduMap/陕西省渭南市华阴市华山风景区玉泉广场北50米路东.jpg");
        wp.setTitle("测试");
        workPublishServiceImpl.save(wp);
        return "success";
    }

结果

时间自动填充

image-20230905142939760

SQL注入器

相当于扩展BaseMapper,添加它没有的方法

编写通用mapper接口

findAll可能会有红线,但不影响使用,不需要加@Component注解

import com.baomidou.entity.Pertwo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;

/**
 *   通用mapper接口,以后创建其他mapper接口时,不再继承BaseMapper,而是继承MyBaseMapper
 */
public interface MyBaseMapper<T> extends BaseMapper<T> {

    //查询所有用户
    public List<Pertwo> findAll();

}

已有mapper继承通用mapper

不需要加@Component注解

/**
*  继承MyBaseMapper, 自定义通用mapper接口
*/
public interface PertwoMapper extends MyBaseMapper<Pertwo> {

}

自定义sql方法类

写一个自定义sql方法类,此类分版本

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.5.3.1</version>
</dependency>

3.5.3.1以前的版本:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;

public class FindAll extends AbstractMethod {
    
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        String sql = "select * from " + tableInfo.getTableName();

        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
		//添加自定义查询sql
        return this.addSelectMappedStatementForOther(mapperClass, "findAll", sqlSource, modelClass);

    }
}

3.5.3.1及其以后

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;

public class FindAll extends AbstractMethod {
	
    //多了构造方法
    public FindAll(String methodName) {
        super(methodName);
    }

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

        String sql = "select * from " + tableInfo.getTableName();

        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);

        return this.addSelectMappedStatementForOther(mapperClass, "findAll", sqlSource, modelClass);

    }
}

除了addSelectMappedStatementForOther添加查询sql外,还有许多其他方法

image-20230905160732501

自定义sql注入器

写一个sql注入器,加入上面自定义sql方法类

3.5.3.1以前的版本:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import java.util.List;

public class MySqlInjector extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //获取框架中已有的注入方法
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //添加自己自定义方法
        methodList.add(new FindAll());

        return methodList;
    }
}

3.5.3.1及其以后的版本:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import java.util.List;

/**
 *   自定义sql注入器
 */
public class MySqlInjector extends DefaultSqlInjector {


    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        //获取框架中已有的注入方法
        List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
        //添加自己自定义方法,参数为自定义方法类中this.addSelectMappedStatementForOther中传入的方法名
        methodList.add(new FindAll("findAll"));

        return methodList;
    }
}

注入SQL注入器

为spring添加注入器Bean

import com.baomidou.injectors.MySqlInjector;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.baomidou.dao") //此处可以不加扫描,但需要添加在主启动类上
public class mpConfig {

    /*** 自定义SQL注入器 */
    @Bean
    public MySqlInjector mySqlInjector() {
        return new MySqlInjector();
    }
    
}

测试

Controller中注入mapper,可能会有红线,不影响使用

@Autowired
private PertwoMapper mapper;

@RequestMapping("/qupt")
public void qust(){
	List<Pertwo> users = mapper.findAll();
	for (Pertwo user : users) {
		System.out.println("findall: "+user);
	}
}

image-20230905160027833

image-20230905160011303

执行SQL分析打印

注意!

  • driver-class-name 为 p6spy 提供的驱动类
  • url 前缀为 jdbc:p6spy 跟着冒号为对应数据库连接地址
  • 打印出 sql 为 null,在 excludecategories 增加 commit
  • 批量操作不打印 sql,去除 excludecategories 中的 batch
  • 批量操作打印重复的问题请使用 MybatisPlusLogFactory (3.2.1 新增)
  • 该插件有性能损耗,不建议生产环境使用。

步骤

pom依赖引入

<dependency>
	<groupId>p6spy</groupId>
	<artifactId>p6spy</artifactId>
	<version>3.9.1</version><!-- 或其他版本 -->
</dependency>            

yml 配置(示例数据库为mysql)

主要是url:jdbc:p6spy:mysql,以及驱动:com.p6spy.engine.spy.P6SpyDriver

spring:
  datasource:
    url: jdbc:p6spy:mysql://localhost:3306/mybatis-plus?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: wangziyu123
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver  #com.mysql.cj.jdbc.Driver

spy.properties

#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory

#3.2.1以下使用或者不配置
#modulelist=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,commit,resultset

# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss

# 实际驱动可多个
#driverlist=org.h2.Driver

# 是否开启慢SQL记录
outagedetection=true

# 慢SQL记录标准 2 秒
outagedetectioninterval=2

最后执行语句测试

效果:

查询消耗5毫秒

image-20230906160215577

数据安全保护

该功能为了保护数据库配置及数据安全,在一定的程度上控制开发人员流动导致敏感信息泄露。

pom引入

版本要大于等于3.3.2

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.3.2</version>
</dependency>

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
	<version>3.5.3.1</version>
</dependency>

使用自带AES工具类加密连接参数

一定要记住并妥善保管密钥,后续用于加解密!!

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
//MybatisPlus自带的AES加密工具
import com.baomidou.mybatisplus.core.toolkit.AES;

@SpringBootTest
public class SampleTest {

    @Test
    public void testSelect() {

        String url = "jdbc:mysql://localhost:3306/mybatis-plus?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8";
        String username = "root";
        String password = "wangziyu123";

        // 生成16位密钥
        String randomKey = AES.generateRandomKey();
        //密钥:3b2dc937dc3030a3
        System.out.println("密钥:" + randomKey);

        // 密钥加密
        String urlRes = AES.encrypt(url, randomKey);
        //B9NhJxO3oceWf1s76AnubjtGTxD4ZO7yrPs9Pm2yEWe7xXAX15VsHO1Vfuva79dWNHh
        System.out.println("url加密后:" + urlRes);

        String unRes = AES.encrypt(username, randomKey);
        //9MTrYDAx6cCZGsTmo15ZhQ==
        System.out.println("username加密后:" + unRes);

        String pasRes = AES.encrypt(password, randomKey);
        //RM1UQWDB7qzlArC84n232A==
        System.out.println("password加密后:" + pasRes);
    }
}

将加密后的信息写入yaml

加密字符串前面要加 mpw:

image-20230906181209611

spring:
  datasource:
    #加密后的地址
    url: mpw:B9NhJxO3oceWf1s76AnubjtGTxD4ZO7yrPs9Pm2yEWe7xXAX15VsHO1Vfuva79dWNHhw3QmVBpGq6cHFRXhos3TaSr+fxq+S2C8+RL8lf+wrsjmQcgA2CxgTfNMEOhA7m1gGxMlPJ0rD11p5t3Bd5L/Eh2w0dNJm/v4l8omS9ozGSKjXBrY6Qy3IHA8LnbqY19bZkdYu06PFCNOvd//MD7fZHfXldSgMbtH2Z/ZG+cwJzh8kXYUPwoxp9pGnW4Sx
    #加密后的用户名
    username: mpw:9MTrYDAx6cCZGsTmo15ZhQ==
    #加密后的密码
    password: mpw:RM1UQWDB7qzlArC84n232A==

在springboot的tomcat配置添加启动参数

--mpw.key=你的密钥

image-20230906181419071

多数据源

pom

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
	<version>3.6.1</version>
</dependency>

yaml配置多数据源

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/mybatis-plus?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8          
          username: root
          password: wangziyu123
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置        
        slave_1:
          url: jdbc:mysql://localhost:3306/mybatis-plus-slave?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: wangziyu123
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        #......省略
        #以上会配置一个默认库master,一个子库slave_1

使用注解@DS(“数据源名”)

@DS可以加载类上或方法上,可以加在mapper接口,或service接口

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.entity.Pertwo;
import java.util.List;

/**
*  继承MyBaseMapper, 自定义通用mapper接口
*/
//@Sharding("mysql")
public interface PertwoMapper extends MyBaseMapper<Pertwo> {
	
    //查的是yaml中master数据库的数
    @DS("master")
    Pertwo selectCode(String code);
	
    //查的是yaml中slave_1数据库的数
    @DS("slave_1")
    List<Pertwo> findAll();
}

MP整体插件

MybatisPlusInterceptor

该插件是核心插件,目前代理了 Executor#queryExecutor#updateStatementHandler#prepare 方法

属性

private List<InnerInterceptor> interceptors = new ArrayList<>();

InnerInterceptor

我们提供的插件都将基于此接口来实现功能

目前已有的功能:

  • 自动分页: PaginationInnerInterceptor
  • 多租户: TenantLineInnerInterceptor
  • 动态表名: DynamicTableNameInnerInterceptor
  • 乐观锁: OptimisticLockerInnerInterceptor
  • sql 性能规范: IllegalSQLInnerInterceptor
  • 防止全表更新与删除: BlockAttackInnerInterceptor

注意:

使用多个功能需要注意顺序关系,建议使用如下顺序

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入

拦截忽略注解 @InterceptorIgnor

属性名是@InterceptorIgnor中的参数,分别屏蔽不同的插件功能

如:@InterceptorIgnor(tenantLine = “true”)

属性名类型默认值描述
tenantLineString“”行级租户
dynamicTableNameString“”动态表名
blockAttackString“”攻击 SQL 阻断解析器,防止全表更新与删除
illegalSqlString“”垃圾 SQL 拦截

分页插件

注意

版本要求:3.4.0 版本以上

1.分页配置

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.baomidou.dao")//主启动类配了这里就可以不配包扫描
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

2.代码配置

Mapper

使用自动生成的即可

public interface UserMapper extends BaseMapper<User> {
    
}

Service

在mybatisx自动生成的service实现中添加分页查询方法,用在Controller中调用

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> page3(){
		
        //1为当前页数,3为每页显示条数
        Page<User> p = new Page<>(1,3);

        Page<User> userPage = userMapper.selectPage(p, null);

        List<User> records = userPage.getRecords();

        return records;
    }

}

Controller

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private UserServiceImpl ui;
    
    //localhost:8080/user/page3
    @RequestMapping("/page3")
    public List<User> page3(){
        return ui.page3();
    }

}

3.代码执行

建表语句

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;


DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `age` int NULL DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1697138078167941125 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;


INSERT INTO `user` VALUES (1, 'Jone', 18, 'test1@baomidou.com');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'test2@baomidou.com');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'test3@baomidou.com');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'test4@baomidou.com');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'test5@baomidou.com');
INSERT INTO `user` VALUES (1696794424765054978, '贝斯特', 16, '12345@qq.com');
INSERT INTO `user` VALUES (1697138078167941121, '阿比西尼亚', 3, '12222@qq.com');
INSERT INTO `user` VALUES (1697138078167941122, '布偶', 3, '33333@qq.com');
INSERT INTO `user` VALUES (1697138078167941123, '豹猫', 2, '66666@qq.com');
INSERT INTO `user` VALUES (1697138078167941124, '暹罗', 1, '88888@qq.com');

SET FOREIGN_KEY_CHECKS = 1;

image-20230913094404824

controller执行结果

http://localhost:8080/user/page3
[{"id":1,"name":"Jone","age":18,"email":"test1@baomidou.com"},{"id":2,"name":"Jack","age":20,"email":"test2@baomidou.com"},{"id":3,"name":"Tom","age":28,"email":"test3@baomidou.com"}]

4.分页类属性解析

public class Page<T> implements IPage<T> {
    private static final long serialVersionUID = 8545996863226528798L;
    
    //用于存储分页查询结果的数据列表
    protected List<T> records;
   	
    //用于表示查询结果的总记录数
    protected long total;
   
    //用于表示分页要查询的 每页记录数
    protected long size;
    
    //表示分页查询中的当前页码,提供了关于正在访问的特定结果页的信息
    protected long current;
    
    //用于存储排序规则,这样可以在查询时指定多个排序条件,以便按照特定顺序对结果进行排序。
    protected List<OrderItem> orders;
   
    //mybatisplus分页Page对象的optimizeCountSql属性是用来优化查询总记录数的功能。默认情况下,当我们使用Page对象进行分页查询时,会先执行一条查询总记录数的SQL语句,然后再执行实际的分页查询SQL语句。通过设置optimizeCountSql属性为true,可以使得在执行查询总记录数时,自动去除掉不必要的order by语句,从而提高查询总记录数的性能。
    //需要注意的是,设置optimizeCountSql属性为true后,可能会导致查询总记录数的结果不准确。因为去除了order by语句,可能会导致查询结果的排序与实际分页查询结果的排序不一致。所以在使用该属性时,需要根据实际情况进行权衡和选择。
    protected boolean optimizeCountSql;
   
    //当searchCount为true时,会在查询数据之前先查询总记录数,然后将总记录数设置到Page对象中;
    //当searchCount为false时,不会进行总记录数的查询,只查询当前页的数据。
    //使用searchCount属性可以控制是否需要查询总记录数,当数据量较大时,查询总记录数可能会影响性能,因此可以通过设置searchCount为false来提高查询效率。但需要注意的是,当searchCount为false时,Page对象中的total属性将会是0,即无法获取到总记录数。
    protected boolean searchCount;
   
    //用来优化查询总记录数的属性
    //通常情况下,获取总记录数的SQL语句会包含多个表的关联查询,这样的查询可能会导致性能下降。而通过设置optimizeJoinOfCountSql属性为true,MyBatis Plus会将原始的查询SQL作为子查询,然后再对子查询进行count操作,从而避免了多表关联查询的性能问题。
    //需要注意的是,使用该属性进行优化时,需要确保查询语句中的表关联条件是正确的,否则可能会导致查询结果不准确。
    protected boolean optimizeJoinOfCountSql;
   
    //用来限制每页查询的最大记录数的
    //当设置了maxLimit属性后,如果查询结果超过了maxLimit指定的值,mybatisplus会自动截取前maxLimit条记录作为当前页的数据返回,而不会返回超过maxLimit的记录
    //这个属性的作用是为了防止一次查询返回过多的数据,导致内存占用过大或者网络传输压力过大。通过限制每页查询的最大记录数,可以有效控制查询结果的大小,提高系统的性能和稳定性。
	//需要注意的是,maxLimit属性只对单次查询有效,如果需要查询更多的记录,可以通过翻页操作来获取后续的数据。
    protected Long maxLimit;
   
    //用来指定统计总记录数的SQL语句的id。在进行分页查询时,除了需要获取当前页的数据,还需要知道总记录数以便于计算总页数等信息。countId属性就是用来指定执行统计总记录数的SQL语句的id。
	//在使用mybatisplus进行分页查询时,我们可以通过设置countId属性来指定执行统计总记录数的SQL语句的id。mybatisplus会根据这个id去执行对应的SQL语句,并将结果作为总记录数返回。
    protected String countId;
}

乐观锁插件

1.概念

乐观锁和悲观锁都是用于解决并发场景下的数据竞争问题,但是却是两种完全不同的思想。它们的使用非常广泛,也不局限于某种编程语言或数据库。

乐观锁的概念:

    指的是在操作数据的时候乐观地认为别人不会同时修改数据,因此默认不上锁,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。

冲突比较少的时候, 使用乐观锁(没有悲观锁那样耗时的开销) ,由于乐观锁的不上锁特性,所以在性能方面要比悲观锁好,比较适合用在DB的读大于写的业务场景

悲观锁的概念:

    指的是在操作数据的时候悲观地认为别人一定会同时修改数据,因此直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。

冲突比较多的时候, 使用悲观锁(没有乐观锁那么多次的尝试),对于每一次数据修改都要上锁,如果在DB读取需要比较大的情况,有线程执行了修改操作,会导致上锁读不出来,等修改线程释放了锁才能读到数据,体验极差。所以比较适合用在DB写大于读的情况

总结:

读取频繁使用乐观锁,写入频繁使用悲观锁。


乐观锁插件:

当要更新一条记录的时候,希望这条记录没有被别人更新

乐观锁实现方式:

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败

2.配置

配置类

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

}

实体类

数据库加version字段

image-20230913115807930

实体类加version字段与@Version注解

import com.baomidou.mybatisplus.annotation.*;
/**
* 乐观锁版本号
*/
@Version
private Long version;
  • 支持的数据类型只有: int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1,也就是version字段自增1

3.执行代码

Controller

  • 必须先进行查询操作,获取实体类得到version,然后修改,才可以让version自增。不是mp从数据库操作中拿出来的实体类,不会自增
  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!
/**
* 将user表中,name='贝斯特'的,按实体类u进行更新,此处实体类赋值了email,那就更新email字段
* 乐观锁操作看下面注释
* */
@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private UserMapper userMapper;
    
    @Autowired
    private UserServiceImpl userService;
    
    //localhost:8080/user/uu
    @RequestMapping("/uu")
    public void uu(){
        
        //1.必须先从数据库取出user,也就是必须先查
        User u = userMapper.selectById(1696794424765054978l);
        //User u = new User(); 这种写法的version虽然能查出来,但是不会自增改变
        u.setEmail("152301@qq.com");
        
        //2.然后修改,才可以让version自增,不是mp从数据库拿出来得实体类,不会自增
        //3.以下3个方法都可以使version生效
        userMapper.updateById(u);       
        userMapper.update(u,new UpdateWrapper<User>().eq("name","贝斯特"));//将name是贝斯特的,按实体类u进行更新
        userService.update(u,new UpdateWrapper<User>().eq("name","贝斯特"));//将name是贝斯特的,按实体类u进行更新
    }

}

验证

第一步,打断点执行过查询,看到查出来版本是1,此时暂停代码执行,断点在update前,不进行sql修改

image-20230913134802302

第二步,将该条数据在数据库中的版本由1改为2,模拟代码执行过程中数据被他人修改的过程

image-20230913134912691

最后,版本号被中途修改,前后对应不上,所以乐观锁生效,没有执行数据更改,Updates: 0

image-20230913135040269

多租户插件

Saas多租户

多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。

Mybatis-Plus中,多租户的实现,实际就是将租户id拼接到了所操作sql的条件中

1.数据表

建表

tenant_id -- 租户ID

租户ID可以是整数或字符串

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  `version` bigint DEFAULT NULL,
  `tenant_id` bigint NOT NULL COMMENT '租户ID', --也可以是varchar类型
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1697138078167941125 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

插入数据

上半部分租户id为1,下半部分租户id为2

INSERT INTO `user` VALUES (1, 'Jone', 18, 'test1@baomidou.com', NULL, 1);
INSERT INTO `user` VALUES (2, 'Jack', 20, 'test2@baomidou.com', NULL, 1);
INSERT INTO `user` VALUES (3, 'Tom', 28, 'test3@baomidou.com', NULL, 1);
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'test4@baomidou.com', NULL, 1);
INSERT INTO `user` VALUES (5, 'Billie', 24, 'test5@baomidou.com', NULL, 1);
---------------------------------------------------------------------------------------
INSERT INTO `user` VALUES (1696794424765054978, '贝斯特', 16, '122222@qq.com', 2, 2);
INSERT INTO `user` VALUES (1697138078167941121, '阿比西尼亚', 3, '12222@qq.com', NULL, 2);
INSERT INTO `user` VALUES (1697138078167941122, '布偶', 3, '33333@qq.com', NULL, 2);
INSERT INTO `user` VALUES (1697138078167941123, '豹猫', 2, '66666@qq.com', NULL, 2);
INSERT INTO `user` VALUES (1697138078167941124, '暹罗', 1, '88888@qq.com', NULL, 2);

image-20230913154212857

2.代码配置

配置插件

注意插件顺序,先多租户,在分页、然后乐观锁

主要进行了三项配置:

  • 获取租户ID,会将此ID值拼接到所有sql的条件中
  • 获取租户表字段 默认为tenant_id
  • 表过滤,哪些表需要进行租户过滤。返回true,表示当前表不进行租户过滤,false则需要过滤
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;


@Configuration
public class MybatisPlusConfig {


    @Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        //多租户插件
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            /**
             * 获取租户ID 实际应该从用户信息中获取,此处为了测试随便写了,就写上面建表的1或2
             * */
            @Override
            public Expression getTenantId() {
                //new StringValue("0001");字符串的
                return new LongValue(2);
            }

            /**
             * 获取租户表字段 默认为tenant_id
             *
             * @return
             */
            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            /**
             * 表过滤,返回true,表示当前表不进行租户过滤
             *
             *  默认返回 false 表示所有表都需要拼多租户条件
             * */
            @Override
            public boolean ignoreTable(String tableName) {
                //排除a表
                //return "a".equalsIgnoreCase(tableName);
                return false;
            }
        }));

        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

    /**
     * 设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题 
     * 新版本、大概3.4之后不需要这个
     * */
//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        return configuration -> configuration.setUseDeprecatedExecutor(false);
//    }
}

实体类

租户id字段,最好就用tenantId

@TableName(value ="user")
public class User implements Serializable {

    /**
     * 租户 ID
     */
    private Long tenantId;
}

多租户屏蔽

如果配置了多租户,又希望某些语句的执行不进行租户过滤,则需要:

xxMapper.java中的方法上添加@InterceptorIgnore(tenantLine = "true")

//注意将泛型换为实体类
public interface UserMapper extends MyUserMapper<User>{
	
	//不希望此方法进行多租户过滤
    @InterceptorIgnore(tenantLine = "true")
    User selectCode(String id);
}

注意点:

  1. 注解要加在xxxMapper.java类的方法上

  2. 访问修饰符default会使注解失效

  3. 如果不是自定义语句,而是MybatisPlus自带的增删改查方法,要继承BaseMapper来添加注解,如下:

    /**
     * 注意将泛型换为实体类
     * */
    public interface UserMapper extends BaseMapper<User>{
        
        //selectById是Mybatis-Plus中BaseMapper<T>自带的
        @InterceptorIgnore(tenantLine = "true")
        User selectById(Serializable id);
    
        //这个是自己写的
        @InterceptorIgnore(tenantLine = "true")
        User selectCode(String id);
    }
    

3.代码执行

Controller

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserServiceImpl userService;
 	
    //localhost:8080/user/zu
    @RequestMapping("/zu")
    public User zu(){
        return userService.getOne(new QueryWrapper<User>().eq("name","贝斯特"));
    }   
}

结果

因为配置类里获取的租户id为2,所以自动拼接了tenant_id = 2

image-20230913150527292

使用了@InterceptorIgnore(tenantLine = "true"),则没有拼接tenant_id = 2,实现了屏蔽

image-20230914141514066

防全表更新删除插件

针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除

其实就是阻止不带where条件的修改或删除语句执行,执行了全表更新删除会报错:

com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: 
	Prohibition of table update operation

1.数据表

建表语句

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  `version` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1697138078167941125 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

插入数据

此处没有沿用上面示例的多租户,因为多租户会拼接sql,带租户id的where条件,不会使此插件生效

INSERT INTO `user` VALUES (1696794424765054978, '贝斯特', 16, '123', 2);
INSERT INTO `user` VALUES (1697138078167941121, '阿比西尼亚', 3, '15230114461@163.com', NULL);
INSERT INTO `user` VALUES (1697138078167941122, '布偶', 3, '457', NULL);
INSERT INTO `user` VALUES (1697138078167941123, '豹猫', 2, '789', NULL);
INSERT INTO `user` VALUES (1697138078167941124, '暹罗', 1, 'sdf', NULL);

2.代码配置

config

注意插件顺序!

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;


@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //防止全表更新与删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

屏蔽全表操作插件

如果希望某些方法可以进行全表更新删除:

xxMapper.java中的方法上添加@InterceptorIgnore(blockAttack = "true")

import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.io.Serializable;


public interface UserMapper extends BaseMapper<User> {

    //这个是自己写的
    @InterceptorIgnore(blockAttack = "true")
    User selectCode(String id);

    //这个是BaseMapper自带的,直接从BaseMapper里粘贴出来加注解即可,注意把泛型T改为对应entity
    @InterceptorIgnore(blockAttack = "true")
    int update(@Param("et") User entity, @Param("ew") Wrapper<User> updateWrapper);

}

3.代码执行

Controller

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private UserMapper userMapper;
    
    //localhost:8080/user/dela
    @RequestMapping("/dela")
    public void dela(){
        User u = new User();
        u.setEmail("qq.com");
        userMapper.update(u,null);
    }

    //localhost:8080/user/delb
    @RequestMapping("/delb")
    public void delb(){
        User u = new User();
        u.setEmail("15230114461@163.com");
        userMapper.update(u,new UpdateWrapper<User>().eq("name","阿比西尼亚"));
    }
    
}

结果

localhost:8080/user/dela

会报错:

image-20230913165943631

localhost:8080/user/delb

执行成功!

image-20230913170928927

动态表名插件

1.概念

描述: Sql执行时,动态的修改表名

简单业务场景: 日志或者其他数据量大的表,通过日期进行了水平分表,需要通过日期参数,动态的查询数据。分开来存储,表中的列名都是一样的,只是表名不同。

**比如:**log_201907、log_201908等等之类的

2.数据表

建表语句

建立一个user表,以年月结尾

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user202308
-- ----------------------------
DROP TABLE IF EXISTS `user202308`;
CREATE TABLE `user202308`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `age` int NULL DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  `version` bigint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1697138078167941125 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user202308
-- ----------------------------
INSERT INTO `user202308` VALUES (1696794424765054978, '贝斯特', 16, '123', 2);
INSERT INTO `user202308` VALUES (1697138078167941121, '阿比西尼亚', 3, '15230114461@163.com', NULL);
INSERT INTO `user202308` VALUES (1697138078167941122, '布偶', 3, '457', NULL);
INSERT INTO `user202308` VALUES (1697138078167941123, '豹猫', 2, '789', NULL);
INSERT INTO `user202308` VALUES (1697138078167941124, '暹罗', 1, 'sdf', NULL);

SET FOREIGN_KEY_CHECKS = 1;

改上面语句表名加一个月,生成两张年月结尾的user表

image-20230914101433694

3.代码配置

实体类、mapper等不需要修改,生成后不动

动态表名转换器

根据当前年月生成表名后缀进行拼接,返回逻辑表名+年月,比如 ‘user202309’

import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ITableNameHandler implements TableNameHandler {

    private String handlerTableName;

    ITableNameHandler(String handlerTableName){
        this.handlerTableName = handlerTableName;
    }

    @Override
    public String dynamicTableName(String sql, String tableName) {
        /**
         * 根据当前年月生成表名后缀进行拼接,返回表名+年月,比如 'user202309'
         * */
        Date da = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");
        String end = simpleDateFormat.format(da);

        return handlerTableName + end;
    }
}

配置类

yaml配置动态表的原始表

DynamicTableName:
  tableName: user

将动态表名转换器配置到动态表插件

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.extension.plugins.inner.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import java.text.SimpleDateFormat;
import java.util.Date;


@Configuration
public class MybatisPlusConfig {

    @Value("${DynamicTableName.tableName}")
    private String handlerTableName;

    @Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        //动态表名插件
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();


        dynamicTableNameInnerInterceptor.setTableNameHandler(new ITableNameHandler(handlerTableName));
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);

        // 3.4.3.2 作废该方式
        // dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);

        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //防止全表更新与删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

}

屏蔽动态表名

如果某些操作不希望使用动态表名

xxMapper.java中的方法上添加@InterceptorIgnore(dynamicTableName= "true")

import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface UserMapper extends BaseMapper<User> {

    @InterceptorIgnore(dynamicTableName = "true")
    List<User> selectByMap(@Param("cm") Map<String, Object> columnMap);

}

这样,selectByMap方法就不会使用转换的表名了

4.代码执行

Controller

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private UserServiceImpl userService;
    
	@RequestMapping("/zu")
    public User zu(){
        return userService.getOne(new QueryWrapper<User>().eq("name","贝斯特"));
    }
    
}

结果

使用了转换的表名

image-20230914102018125

数据权限插件

Mybatis-Plus的数据权限插件,其实就是拦截sql,在其末尾添加where条件。

比如部门、用户id等,实现操作本部门、本用户的数据

1.建表

主要添加部门及用户id字段

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `age` int NULL DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  `version` bigint NULL DEFAULT NULL,
  `deptid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '部门',
  `userid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1697138078167941125 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'Jone', 18, 'qq.com', NULL, 'A', 'Jone');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'qq.com', NULL, 'A', 'Jack');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'qq.com', NULL, 'A', 'Tom');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'qq.com', NULL, 'A', 'Sandy');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'qq.com', NULL, 'A', 'Billie');
INSERT INTO `user` VALUES (1696794424765054978, '贝斯特', 16, 'qq.com', 2, 'B', '贝斯特');
INSERT INTO `user` VALUES (1697138078167941121, '阿比西尼亚', 3, 'qq.com', NULL, 'B', '阿比西尼亚');
INSERT INTO `user` VALUES (1697138078167941122, '布偶', 3, 'qq.com', NULL, 'B', '布偶');
INSERT INTO `user` VALUES (1697138078167941123, '豹猫', 2, 'qq.com', NULL, 'B', '豹猫');
INSERT INTO `user` VALUES (1697138078167941124, '暹罗', 1, 'qq.com', NULL, 'B', '暹罗');

SET FOREIGN_KEY_CHECKS = 1;

2.代码配置

2.1 权限工具类

写一个保存权限信息的工具类

这里权限就用简单的数字代替了

public class GetRole {

    private static ThreadLocal<Integer> role = new ThreadLocal<Integer>();

    public static Integer getRole(){

        return role.get();
    }

    public static void setRole(Integer i){
        role.set(i);
    }

}
2.2 拦截器

写一个拦截器实现类

role==2 为部门权,限查本部门

role==3 为本人权限,只能查本人数据

由于只是演示,部门写死为"B",用户写死为"贝斯特"

import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;

/**
 * Mybatis-Plus的数据权限插件,其实就是拦截sql,在其末尾添加where条件。
 * 比如部门、用户id等,实现操作本部门、本用户的数据
 * */
public class MyDataPermissionHandler implements DataPermissionHandler {

    /**
     * 获取数据权限 SQL 片段
     *
     * Params:
     * where – 待执行sql的Where条件
     * mappedStatementId – 待执行sql所对应的Mapper方法,
     *
     * Returns:
     * JSqlParser 条件表达式,返回的条件表达式会覆盖原有的条件表达式
     * */
    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
		
        //一般用户登陆后信息保存在ThreadLocal中,这里模拟这个过程,从ThreadLocal获取权限字符,2或3
        Integer role = GetRole.getRole();
        
        Expression sqlSegmentExpression = null;

        try {
            /**
             * 如果要执行的sql没有where条件, CCJSqlParserUtil.parseCondExpression("A=A") 会自动加where条件
             * 比如执行的是 SELECT id, deptid FROM user,最后会变为 SELECT id, deptid FROM user where A=A
             * */
            if(where == null){

                if(role==2){
                    return CCJSqlParserUtil.parseCondExpression("deptid = 'B' ");
                } else if (role==3){
                    return CCJSqlParserUtil.parseCondExpression("userid = '贝斯特' ");
                }

            }else{

                if(role==2){
                    sqlSegmentExpression = CCJSqlParserUtil.parseCondExpression("deptid = 'B' ");
                } else if (role==3){
                    sqlSegmentExpression = CCJSqlParserUtil.parseCondExpression("userid = '贝斯特' ");
                }

            }

        } catch (JSQLParserException e) {
            e.printStackTrace();
            return null;
        }
        //返回and....的语句
        //参数where放到and的左边,参数sqlSegmentExpression放and右边,where and sqlSegmentExpression
        return new AndExpression(where,sqlSegmentExpression);

    }

}
2.3 配置类

将拦截器实现类配置到插件主体中

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.inner.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;


@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //数据权限
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
		//分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //防止全表更新与删除
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

3.执行测试

测试调用

模拟传入用户权限为2,传入ThreadLocal,并在拦截器获取用于拼接sql判断

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private UserMapper userMapper;
    
	 //localhost:8080/user/ro?r=2
    @RequestMapping("/ro")
    public List<User> ro(@RequestParam Integer r){

        GetRole.setRole(r);
        List<User> ul = userMapper.selectList(new QueryWrapper<User>().eq("email","qq.com"));
        return ul;
        
    } 
}

最后拼接了部门条件

image-20230915144517682

数据变动记录插件

必须使用逻辑删除,只针对新增和修改(因为用了逻辑删除,就没有真正的删除了,删除也是修改)

在执行增改操作后,控制台日志会生成json串

1.数据表

要有逻辑删除字段

image-20230916093146672

CREATE TABLE `pertwo0` (
  `deleted` int DEFAULT '0' COMMENT '是否被删除 0 未删除 1 已删除',
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `code` varchar(255) NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

2.实体类及yaml

实体类

逻辑删除字段

@TableName(value ="pertwo",autoResultMap = true)
public class Pertwo implements Serializable {
    /**
     * 是否被删除 0 未删除 1 已删除
     */
    private Integer deleted;
}

yaml

配置逻辑删除

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 全局逻辑删除的实体字段名,也可实体类字段上加上@TableLogic注解
      logic-not-delete-value: 0  # 逻辑未删除值
      logic-delete-value: 1  # 逻辑已删除值

3.插件配置

@Configuration
public class MybatisPlusConfig {

	@Bean
    public MybatisPlusInterceptor mybatisPInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
		//数据变动记录插件
        interceptor.addInnerInterceptor(new DataChangeRecorderInnerInterceptor());

        return interceptor;
    }

}

4.执行测试

Controller

//  localhost:8080/user/inpt
    @RequestMapping("/inpt")
    public String inst(){
        Pertwo p = new Pertwo();
        p.setCode("cat");
        p.setSex("女");
        p.setName("gril");
        //list变为varchar字符串存入数据库
        List<String> contact = new ArrayList<>();
        contact.add("一");
        contact.add("个");
        contact.add("cat");
        p.setJob(contact);

        p.setAge(AgeEnum.TWO);
        pertwoServiceImpl.save(p);
        return "success";
    }

结果

image-20230916091246429

{
	"tableName": "pertwo",
	"operation": "insert",
	"recordStatus": "true",
	"changedData": [{
		"CODE": "null->cat",
		"JOB": "null->[一, 个, cat]",
		"NAME": "null->gril",
		"AGE": "null->TWO",
		"SEX": "null->女"
	}],
	"cost(ms)": 22
}

结合Sharding分库分表

MyBatis-Plus 和 Sharding-JDBC 的版本对应关系如下:

MyBatis-Plus 版本Sharding-JDBC 版本
3.0.0及以上4.0.0及以上
3.0.0以下3.0.0以下

因此,如果使用 MyBatis-Plus 3.0.0及以上版本,应该使用 Sharding-JDBC 4.0.0及以上版本。如果使用 MyBatis-Plus 3.0.0以下版本,应该使用 Sharding-JDBC 3.0.0以下版本。
需要注意的是,不同版本的 MyBatis-Plus 和 Sharding-JDBC 可能存在兼容性问题。为了避免出现这种问题,建议在使用时使用相应版本的 MyBatis-Plus 和 Sharding-JDBC。

准备工作:

建表

在两个库中分别执行下面语句来建表,deleted为逻辑删除字段、age为通用枚举,沿用了上面的示例

先建立pertwo,利用MybatisX工具生成实体类后,在建pertwo0与pertwo1

CREATE TABLE `pertwo` (
  `deleted` int DEFAULT '0' COMMENT '是否被删除 0 未删除 1 已删除',
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `code` varchar(255) NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `pertwo0` (
  `deleted` int DEFAULT '0' COMMENT '是否被删除 0 未删除 1 已删除',
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `code` varchar(255) NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `pertwo1` (
  `deleted` int DEFAULT '0' COMMENT '是否被删除 0 未删除 1 已删除',
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` varchar(255) DEFAULT NULL,
  `code` varchar(255) NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

image-20230908171905659

pom依赖
	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath/>
    </parent>

    <dependencies>

        <!-- spring-boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.16</version>
        </dependency>

        <!--sharding-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-core-common</artifactId>
            <version>4.1.1</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

注意,连接池要用druid而不是druid-spring-boot-starter

如果使用下面这个:

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.2.16</version>
</dependency>

会报错:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

实体类

遇到的问题:

Sharding-JDBC与Myabtis-Plus并非完全兼容,在二者整合情况下。MP的通用枚举功能不适用,所以需要自己手动对二者的转换进行处理,其他不适配功能暂时没发现。

import com.baomidou.Enum.AgeEnum;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.typeHandler.GenericEnumTypeHandler;
import com.baomidou.typeHandler.ListTypeHandler;
import java.io.Serializable;
import java.util.List;

/**
 * @TableName pertwo
 */
@TableName(value ="pertwo",autoResultMap = true)
public class Pertwo implements Serializable {

    @TableId
    private String code;
    /**
     * 是否被删除 0 未删除 1 已删除
     */
    private Integer deleted;
    
    private String name;
    
    //配置数据库int类型与java枚举的互相转换
    //Sharding-JDBC与Myabtis-Plus并非完全兼容,在二者整合情况下。MP的通用枚举功能不适用,所以需要自己手动对二者的转换进行处理
    @TableField(typeHandler = GenericEnumTypeHandler.class)
    private AgeEnum age;
    public AgeEnum getAge() {
        return age;
    }
    public void setAge(AgeEnum age) {
        this.age = age;
    }

    private String sex;

    /**
     *  配置数据库varchar类型与json类型转换,字段类型处理器功能二者目前适配
     */
    @TableField(typeHandler = ListTypeHandler.class)
    private List<String> job;

    public List<String> getJob() {
        return job;
    }

    public void setJob(List<String> job) {
        this.job = job;
    }
    
    @TableField(exist = false)//不作为表中字段,排除出去
    private static final long serialVersionUID = 1L;
    
    public String getCode() {
        return code;
    }
    
    public void setCode(String code) {
        this.code = code;
    }

    /**
     * 是否被删除 0 未删除 1 已删除
     */
    public Integer getDeleted() {
        return deleted;
    }

    /**
     * 是否被删除 0 未删除 1 已删除
     */
    public void setDeleted(Integer deleted) {
        this.deleted = deleted;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
    
}
枚举

在真实开发中,枚举可能会对应下拉框小代码业务。而且不可能只有一种枚举类。

这里定义了共有接口,并在具体枚举中添加静态方法,用于在枚举类型处理器获取所有枚举类型

public interface EnumCommonMethod {    
    Integer getId();
    String getDesc();
}
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

public enum AgeEnum implements EnumCommonMethod
{

    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");

    /**
     * 数据库存放数值,前端展示汉字
     * */
    @JsonValue
    private String desc;
    //@EnumValue
    private int id;

    AgeEnum(int i, String age) {
        this.id = i;
        this.desc = age;
    }

    public static  AgeEnum getEnumById(int id){
        for(AgeEnum ageEnum : AgeEnum.values()){
            if(ageEnum.getId() == id){
                return ageEnum;
            }
        }
        return null;
    }

    @Override
    public Integer getId() {
        return this.id;
    }

    @Override
    public String getDesc(){
        return this.desc;
    }

}

枚举类型处理器

Sharding-JDBC与Myabtis-Plus并非完全兼容,在二者整合情况下。MP的通用枚举功能不适用,所以需要自己手动对二者的转换进行处理

java反射中,Method的invoke如果不传入目标对象实例,则执行的方法必须是静态方法,此处对应上面枚举类中的静态方法

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class GenericEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private Class<E> type;
    private Method getId;
    private Method getEnumById;

    public GenericEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        try {
            getId = type.getDeclaredMethod("getId");
            getEnumById = type.getDeclaredMethod("getEnumById", int.class);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Can not found getId or getEnumById method in " + type.getName(), e);
        }
    }
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        try {
            ps.setInt(i, (Integer)getId.invoke(parameter));
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        try {
            int id = rs.getInt(columnName);
            this.type.getName();
            
            //invoke如果不传入目标示例,则执行的方法必须是静态方法,对应枚举类中的静态方法
            return (E)getEnumById.invoke(null, id);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        try {
            int id = rs.getInt(columnIndex);
            
            //invoke如果不传入目标示例,则执行的方法必须是静态方法,对应枚举类中的静态方法
            return (E)getEnumById.invoke(null, id);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        try {
            int id = cs.getInt(columnIndex);
            
            //invoke如果不传入目标示例,则执行的方法必须是静态方法,对应枚举类中的静态方法
            return (E)getEnumById.invoke(null, id);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
}
字段类型处理器

此处对应实体类job字段的字符串转集合转换处理

import lombok.extern.java.Log;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

@MappedJdbcTypes(JdbcType.VARCHAR)  //数据库类型
@MappedTypes({List.class})          //java数据类型
@Log
public class ListTypeHandler implements TypeHandler<List<String>> {


    @Override
    public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType)
            throws SQLException {

        String hobbys = dealListToOneStr(parameter);
        ps.setString(i , hobbys);
    }


    /**
     * 集合拼接字符串
     * @param parameter
     * @return
     */
    private String dealListToOneStr(List<String> parameter){

        if(parameter == null || parameter.size() <=0) {
            return null;
        }

        String res = "";

        for (int i = 0 ;i < parameter.size(); i++) {
            if(i == parameter.size()-1){
                res+=parameter.get(i);
                return res;
            }
            res+=parameter.get(i)+",";
        }
        return null;

    }

     

    @Override
    public List<String> getResult(ResultSet rs, String columnName)

            throws SQLException {

        log.info("method ====>>> getResult(ResultSet rs, String columnName)");

        return Arrays.asList(rs.getString(columnName).split(","));
    }

 

    @Override
    public List<String> getResult(ResultSet rs, int columnIndex)

            throws SQLException {

        log.info("method ====>>> getResult(ResultSet rs, int columnIndex)");

        return Arrays.asList(rs.getString(columnIndex).split(","));

    }

    @Override
    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException{

        log.info("method ====>>> getResult(CallableStatement cs, int columnIndex)");

        String hobbys = cs.getString(columnIndex);

        return Arrays.asList(hobbys.split(","));

    }

}

yaml配置

server:
  port: 8080

spring:
  #可以不用配置
  #datasource:
  #url:
  #username:
  #password:
  main:
    allow-bean-definition-overriding: true

  shardingsphere:
    # 参数配置,显示sql
    props:
      sql:
        show: true
    # 配置数据源
    datasource:
      # 给每个数据源取别名,下面的ds1,ds1任意取名字
      names: ds0,ds1
      # 给master-ds1每个数据源配置数据库连接信息
      ds0:
        # 配置druid数据源
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis-plus?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: wangziyu123
        maxPoolSize: 100
        minPoolSize: 5
      # 配置ds1-slave
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis-plus-slave?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: wangziyu123
        maxPoolSize: 100
        minPoolSize: 5

    # 配置默认数据源ds0
    sharding:
      # 默认数据源,如果配置了数据库的读写分离就需要此配置,主要用于写。
      #default-data-source-name: ds0

      # 配置分表的规则
      tables:
        #逻辑表名,去掉数字后缀的表名
        pertwo:
          # 数据节点:数据源$->{0..N}.逻辑表名$->{0..N}
          actual-data-nodes: ds$->{0..1}.pertwo$->{0..1}
          # 拆分库策略,也就是什么样子的数据放入放到哪个数据库中。
          database-strategy:
            # 精确分片策略
            standard:
              sharding-column: sex    # 分片字段(分片键)
              #自定义分片策略
              precise-algorithm-class-name: com.sharding.config.DatabasePreciseShardingAlgorithm
            # 拆分表策略,也就是什么样子的数据放入放到哪个数据表中。
          table-strategy:
            inline:
              sharding-column: age    # 分片字段(分片键)
              #取模策略,如果age字段除2余数为0,就放入表pertwo0,余数为1就放入pertwo1.(余数永远小于除数)
              algorithm-expression: pertwo$->{age % 2} # 分片算法表达式


########################################Mybatis-Plus部分##################################################
#mybatis plus是独立节点,需要单独配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 全局逻辑删除的实体字段名,也可实体类字段上加上@TableLogic注解
      logic-not-delete-value: 0  # 逻辑未删除值
      logic-delete-value: 1  # 逻辑已删除值
  configuration:
    #    控制台打印sql语句方便调试sql语句执行错误
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #org.apache.ibatis.logging.log4j2.Log4j2Impl  这个不在控制台打印查询结果,但是在log4j中打印
    #org.apache.ibatis.logging.nologging.NoLoggingImpl  在生产环境,不打印SQL日志
    #    log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl

  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
  #  mapper-locations: classpath*:mapper/**/*Mapper.xml
  mapper-locations: classpath*:mapper/*.xml,classpath*:mapper/**/*Mapper.xml
  # 实体扫描,多个package用逗号或者分号分隔
  type-aliases-package: com.sharding.entity
连接池问题

整合sharding,在配置数据源datasource时,除了url、username、password等,type也必须配,否则会报错

Spring框架中,spring.datasource.type属性用于指定数据源的类型。它可以配置以下几种类型:

  1. com.zaxxer.hikari.HikariDataSource:使用HikariCP连接池作为数据源。
  2. org.apache.tomcat.jdbc.pool.DataSource:使用Tomcat JDBC连接池作为数据源。
  3. org.apache.commons.dbcp2.BasicDataSource:使用Apache Commons DBCP连接池作为数据源。
  4. org.springframework.jdbc.datasource.SimpleDriverDataSource:简单的数据源实现,直接使用提供的JDBC驱动程序。

除了上述几种类型,还可以根据需要自定义数据源类型。使用不同的数据源类型可以根据具体的需求选择适合的连接池或驱动程序。

此处我们的type配置druid连接池:

type: com.alibaba.druid.pool.DruidDataSource

如果多个数据源的连接池参数一致,可采取如下:

不再为每个数据源单独配置连接池参数,而是使用一份配置:

spring:

  shardingsphere:
	
	#省略
	#.......
	
    datasource:
      names: ds0,ds1
      
      ds0:        
        type: com.alibaba.druid.pool.DruidDataSource # 配置druid数据源
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: 
        username: root
        password: wangziyu123
        
        #不再为每个数据源单独配置连接池参数
        #maxPoolSize: 100
        #minPoolSize: 5
        
      ds1:        
      	type: com.alibaba.druid.pool.DruidDataSource # 配置druid数据源
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: 
        username: 
        password: 
        
        #不再为每个数据源单独配置连接池参数
        #maxPoolSize: 100
        #minPoolSize: 5
        
    #而是使用一份配置    
    #此处的连接池配置会被所有数据源统一使用    
	druid:
    	# 初始连接数
    	initialSize: 5
    	# 最小连接池数量
    	minIdle: 10
    	# 最大连接池数量
    	maxActive: 20
    	# 配置获取连接等待超时的时间
    	maxWait: 60000
    	# 配置连接超时时间
    	connectTimeout: 30000
    	# 配置网络超时时间
    	socketTimeout: 60000

druid监控页面问题

整合sharding连接池要用druid,而不是druid-spring-boot-starter,所以无法使用yaml配置监控页面

使用代码进行配置

注意只是配置监控页面,其他连接池参数还是使用yaml配置

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DruidConfig {

    /**
     * Druid监控
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){

        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

        Map<String,String> initParams = new HashMap<>();//这是配置的druid监控的登录密码
        initParams.put("loginUsername","root");
        initParams.put("loginPassword","root");

        //白名单,不配置默认就是允许所有访问
        //initParams.put("allow","");

        //黑名单IP
        //initParams.put("deny","192.168.15.21");

        bean.setInitParameters(initParams);

        return bean;

    }

    /**
     * web监控的filter
     */
    @Bean
    public FilterRegistrationBean webStatFilter(){
		
        WebStatFilter ws = new WebStatFilter();
        //开启session统计功能,默认1000个
        ws.setSessionStatEnable(true);
        
        FilterRegistrationBean bean = new FilterRegistrationBean();       
        bean.setFilter(ws);
        
        Map<String,String> initParams = new HashMap<>();
		//过滤掉需要监控的文件
        initParams.put("exclusions","/static/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
       
        bean.setInitParameters(initParams);
        bean.setUrlPatterns(Arrays.asList("/*"));
        
        return  bean;
    }
}

去掉监控页面的连接池yaml配置

spring:
  shardingsphere:
  #......具体配置去上面示例粘贴
  sharding:
  #......具体配置去上面示例粘贴
  
druid:
    # 初始连接数
    initialSize: 5
    # 最小连接池数量
    minIdle: 10
    # 最大连接池数量
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置连接超时时间
    connectTimeout: 30000
    # 配置网络超时时间
    socketTimeout: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    # 配置一个连接在池中最大生存的时间,单位是毫秒
    maxEvictableIdleTimeMillis: 900000
    # 配置检测连接是否有效
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    filter:
      stat:
        enabled: true
        # 慢SQL记录
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: true
      wall:
        config:
          multi-statement-allow: true
    filters: stat,wall,log4j2

监控页面预览

访问地址:

http://localhost:8080/druid

可以看到是两个数据源

image-20230911102255044

分片策略配置

对应yaml中的precise-algorithm-class-name

import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;


@Slf4j
public class DatabasePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    public DatabasePreciseShardingAlgorithm() {
    }

    /**
     * 以拼接逻辑表名末尾数字的方式实现分表:
     *  性别为'女',就将'0'拼接到逻辑表名末尾返回
     *  性别为'男',就将'1'拼接到逻辑表名末尾返回
     * 
     * @param dataSourceCollection 数据源列表
     * @param preciseShardingValue 分片信息,包括分片字段,逻辑表等                         
     * */
    @Override
    public String doSharding(Collection<String> dataSourceCollection, PreciseShardingValue<String> preciseShardingValue) {
        // 获取分片键的值,也就是分片字段的值
        String shardingValue = preciseShardingValue.getValue();
        // 获取逻辑表
        String logicTableName = preciseShardingValue.getLogicTableName();
        log.info("分片键的值:{},逻辑表:{}", shardingValue, logicTableName);
        
        String endMath = "";
        if("女".equals(shardingValue)){
            endMath = "0";
        }else if("男".equals(shardingValue)){
            endMath = "1";
        }else{
            log.info("字段:"+preciseShardingValue.getColumnName()+"的实际值为: "+shardingValue+",不符合规定值");
        }

        // 遍历数据源
        for (String databaseSource : dataSourceCollection) {
            // 判断数据源是否存在
            if (databaseSource.endsWith(endMath)) {
                return databaseSource;
            }
        }
        // 不存在则抛出异常
        throw new UnsupportedOperationException();
    }
}

测试

mapper

在自动生成基础添加

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.baomidou.dao.PertwoMapper">

   <resultMap id="BaseResultMap" type="com.baomidou.entity.Pertwo">
            <id property="code" column="code" jdbcType="VARCHAR"/>
            <result property="deleted" column="deleted" jdbcType="INTEGER"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="sex" column="sex" jdbcType="VARCHAR"/>
            <result property="job" column="job" jdbcType="VARCHAR" typeHandler="com.baomidou.typeHandler.ListTypeHandler"/>
    </resultMap>

    <sql id="Base_Column_List">
       select  code,deleted,name,
        age,sex,job from pertwo
    </sql>


    <select id="selectCode" parameterType="String" resultMap="BaseResultMap">
        <include refid="Base_Column_List"/>
        where code = #{code}
    </select>

</mapper>

mapper接口

import com.baomidou.entity.Pertwo;
import java.util.List;

/**
*  继承MyBaseMapper, 自定义通用mapper接口
*/
public interface PertwoMapper extends BaseMapper<Pertwo> {

    Pertwo selectCode(String code);

}
service

接口

import com.baomidou.entity.Pertwo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface PertwoService extends IService<Pertwo> {
    Pertwo selectCode(String code);
}

实现类

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.entity.Pertwo;
import com.baomidou.service.PertwoService;
import com.baomidou.dao.PertwoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PertwoServiceImpl extends ServiceImpl<PertwoMapper, Pertwo>
    implements PertwoService{

    @Autowired
    private PertwoMapper pw;

    @Override
    public Pertwo selectCode(String code) {

        return pw.selectCode(code);
    }
}
Controller
@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private PertwoServiceImpl pertwoServiceImpl;
    

	//localhost:8080/user/mupt
    @RequestMapping("/mupt")
    public Pertwo must(){
        //Pertwo p1 = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("age",AgeEnum.THREE));
        Pertwo p = pertwoServiceImpl.getOne(new QueryWrapper<Pertwo>().eq("sex","男"));
        return p;
    }

    //localhost:8080/user/qpt?code=lyh
    @RequestMapping("/qpt")
    public Pertwo qst(@RequestParam String code){
        Pertwo p = pertwoServiceImpl.selectCode(code);
        return p;
    }

    //localhost:8080/user/inpt
    @RequestMapping("/inpt")
    public String inst(){
        Pertwo p = new Pertwo();
        p.setCode("cat");
        p.setSex("女");
        p.setName("gril");
        //list变为varchar字符串存入数据库
        List<String> contact = new ArrayList<>();
        contact.add("一");
        contact.add("个");
        contact.add("cat");
        p.setJob(contact);
        p.setAge(AgeEnum.TWO);
        pertwoServiceImpl.save(p);
        return "success";
    }

    //localhost:8080/user/inpt1
    @RequestMapping("/inpt1")
    public String inst1(){
        Pertwo p = new Pertwo();
        p.setCode("dog");
        p.setSex("男");
        p.setName("boy");
        //list变为varchar字符串存入数据库
        List<String> contact = new ArrayList<>();
        contact.add("一");
        contact.add("个");
        contact.add("dog");
        p.setJob(contact);
        p.setAge(AgeEnum.THREE);
        pertwoServiceImpl.save(p);
        return "success";
    }

}
结果:

localhost:8080/user/inpt:
cat 主0

localhost:8080/user/inpt1

dog slave 1

分库分表策略

分库分表的策略,有5种

package org.apache.shardingsphere.core.yaml.config.sharding;

public final class YamlShardingStrategyConfiguration implements YamlConfiguration {
    
    //单分片键的标准分片策略,支持精确查询和范围查询。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。
    private YamlStandardShardingStrategyConfiguration standard;
    
    //复合分片策略,支持多分片键组合。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持
    private YamlComplexShardingStrategyConfiguration complex;
    
    //通过Hint而非SQL解析的方式分片的策略。简单来理解就是说,他的分片键不再跟SQL语句相关联,而是用程序另行指定。
    private YamlHintShardingStrategyConfiguration hint;
    
    //行表达式分片策略
    private YamlInlineShardingStrategyConfiguration inline;
    
    //不分片的策略
    private YamlNoneShardingStrategyConfiguration none;
}

yaml种对应的配置

分库与分表是通用的

分库的:

image-20230911151136357

分表的:

image-20230911150954134

标准分片策略
  • 只支持单分片键,也就是一个数据库字段的分片。

  • 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。相当于等于一个值,或在几个值范围中

public final class YamlStandardShardingStrategyConfiguration implements YamlBaseShardingStrategyConfiguration {
    //指定分片键。也就是数据库的列,字段名
    private String shardingColumn;
    //指定一个精确查询的实现类,值为全类名。用于处理:分片键=某个具体值
    private String preciseAlgorithmClassName;
    //指定一个范围查询的实现类,值为全类名。用于处理:分片键in或BETWEEN AND某些值,即范围处理
    private String rangeAlgorithmClassName;
}

对应下面的database-strategytable-strategy的yaml配置:

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
       #....
      ds1:
       #....   
    sharding:
      tables:      
        pertwo:       
          actual-data-nodes: ds$->{0..1}.pertwo$->{0..1}
          
          #分库
          database-strategy:
            # 精确分片策略
            standard:
              # 分片字段(分片键)
              sharding-column: sex    
              # 自定义精确分片策略实现类
              precise-algorithm-class-name: com.sharding.config.DatabasePreciseShardingAlgorithm
          
          #分表
          table-strategy:
          	# 精确分片策略
            standard:
              # 分片字段(分片键)
              sharding-column: age    
              # 自定义精确分片策略实现类
              precise-algorithm-class-name: com.sharding.config.DataTablePreciseShardingAlgorithm

分库实现类代码示例:

  • 精确查询实现接口PreciseShardingAlgorithm

  • 范围查询实现接口RangeShardingAlgorithm

下面示例为精确查询:

public class DatabasePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    public DatabasePreciseShardingAlgorithm() {
    }

    /**
     * 以拼接逻辑表名末尾数字的方式实现分表:
     *  性别为'女',就将'0'拼接到逻辑表名末尾返回,也就是sex为女的使用pertwo0表
     *  性别为'男',就将'1'拼接到逻辑表名末尾返回,也就是sex为男的使用pertwo1表
     * 
     * @param dataSourceCollection 数据源列表
     * @param preciseShardingValue 分片信息,包括分片字段,逻辑表等                         
     * */
    @Override
    public String doSharding(Collection<String> dataSourceCollection, PreciseShardingValue<String> preciseShardingValue) {
        // 获取分片键的值,也就是分片字段的值
        String shardingValue = preciseShardingValue.getValue();
        // 获取逻辑表
        String logicTableName = preciseShardingValue.getLogicTableName();
        log.info("分片键的值:{},逻辑表:{}", shardingValue, logicTableName);
        
        String endMath = "";
        if("女".equals(shardingValue)){
            endMath = "0";
        }else if("男".equals(shardingValue)){
            endMath = "1";
        }else{
            log.info("字段:"+preciseShardingValue.getColumnName()+"的实际值为: "+shardingValue+",不符合规定值");
        }

        // 遍历数据源
        for (String databaseSource : dataSourceCollection) {
            // 判断数据源是否存在
            if (databaseSource.endsWith(endMath)) {
                return databaseSource;
            }
        }
        // 不存在则抛出异常
        throw new UnsupportedOperationException();
    }
}
复合分片策略

提供多个数据库字段对SQL语句中的=, IN和BETWEEN AND的分片操作支持

@Getter
@Setter
public final class YamlComplexShardingStrategyConfiguration implements YamlBaseShardingStrategyConfiguration {
    //分片键(多个)
    private String shardingColumns;
    //实现类的全类名
    private String algorithmClassName;
}

yaml配置

spring:
  shardingsphere:
    sharding:
      default-table-strategy:
        #复合分片
        complex:
          #分片键(多个)
          sharding-columns:
          #实现类的全类名
          algorithm-class-name:

实现类示例

待补充…

Hint分片策略
package org.apache.shardingsphere.core.yaml.config.sharding.strategy;

@Getter
@Setter
public final class YamlHintShardingStrategyConfiguration implements YamlBaseShardingStrategyConfiguration {
    #实现类全类名
    private String algorithmClassName;
}

yaml配置

spring:
  shardingsphere:
    sharding:
      default-table-strategy:
        #Hint分片策略
        hint:
          #实现类全类名
          algorithm-class-name: 

实现类示例

待补充…

表达式分片策略

支持单分片键,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。

如: tuser${user_id % 8} 表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0到t_user_7。

package org.apache.shardingsphere.core.yaml.config.sharding.strategy;

@Getter
@Setter
public final class YamlInlineShardingStrategyConfiguration implements YamlBaseShardingStrategyConfiguration {
    //分片键,单个
    private String shardingColumn;
    //表达式
    private String algorithmExpression;
}

yaml配置

spring:
  shardingsphere:
    sharding:
      tables:
        pertwo:
          table-strategy:
          	#表达式策略
            inline:
              # 分片字段(分片键)
              sharding-column: age    
              #取模策略,如果age字段除2余数为0,就放入表pertwo0,余数为1就放入pertwo1.(余数永远小于除数)
              algorithm-expression: pertwo$->{age % 2} # 分片算法表达式

代码示例

表达式策略无需写代码配置

none分片策略

不分片的策略。什么都没有

package org.apache.shardingsphere.core.yaml.config.sharding.strategy;

public final class YamlNoneShardingStrategyConfiguration implements YamlBaseShardingStrategyConfiguration {
    
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值