Mybatis Plus:基础使用

3/26/2021 Mybatis

# 序言

MyBatis-Plus(简称 MP)是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

# 环境

假设您已经

  • 拥有 Java 开发环境以及IDEA
  • 熟悉 Spring Boot
  • 熟悉 Maven
  • 熟悉MySQL

直接在idea使用spring initializr初始化一个springboot项目+mysql驱动+lombok (opens new window)(简化实体类),同时idea也得装lombok插件

构建一个简单的数据库和一个简单的表并插入几条数据

CREATE DATABASE db_test;
USE db_test;
CREATE TABLE t_test ( `id` BIGINT NOT NULL PRIMARY KEY, `name` VARCHAR ( 24 ) );
INSERT INTO t_test VALUES( 1, "张三" );
INSERT INTO t_test VALUES( 2, "李四" );
INSERT INTO t_test VALUES( 3, "王五" );
1
2
3
4
5
6

导入mybatis plus依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>
1
2
3
4
5

在application.yml配置数据库信息(默认是properties格式,改成yml即可)

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_test?serverTimezone=UTC
    username: root
    password: 123456
1
2
3
4
5
6
7
8
9

在application.yml启动 mybatis 本身的 log 日志

# 方式一
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
    
# 方式二 application.yml 中增加配置,指定 mapper 文件所在的包
logging:
  level:
    com.baomidou.example.mapper: debug
1
2
3
4
5
6
7
8
9

springboot启动类添加注解

@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
1

一般我们自己还会写mapper,所以还得加上自己mapper包名

@MapperScan({"com.baomidou.mybatisplus.samples.quickstart.mapper","xyz.ayay.mpdemo.mapper"})
1

新建entity包和mapper包

例:xyz.ayay.mpdemo.entity

编写entity类及mapper类

@Data
public class TTest {
    private Long id;
    private String name;
}
1
2
3
4
5
@Mapper
public interface TTestMapper extends BaseMapper<TTest> {
}
1
2
3

准备过程至此结束

# 简单的CRUD

上面过程准备好就可以在test目录下编写测试案例了

首先注入mapper,因为之后的操作都得通过mapper来进行

@Autowired
private TTestMapper tTestMapper;
1
2

# 查询

@Test
public void testSelect() {
    //通过id查询单个记录
    TTest tTest1 = tTestMapper.selectById(1L); // TTest(id=1, name=张三)
    // 查询记录数量
    Integer count = tTestMapper.selectCount(null); // 3
    // 批量查询 可加wrapper条件
    List<TTest> tTests = tTestMapper.selectList(null);
    /*
    TTest(id=1, name=张三)
    TTest(id=2, name=李四)
    TTest(id=3, name=王五)
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 新增

@Test
public void testInsert() {
    TTest testObj = new TTest();
    testObj.setId(4L);
    testObj.setName("赵六");
    tTestMapper.insert(testObj);
}
1
2
3
4
5
6
7

没有出错的话刷新一下表,就能看到记录已经成功的被插入到表中了

# 修改

@Test
public void testUpdate() {
    TTest testObj = new TTest();
    testObj.setId(4L);
    testObj.setName("赵六update");
    tTestMapper.updateById(testObj);
}
1
2
3
4
5
6
7

# 删除

@Test
public void testDelete() {
	tTestMapper.deleteById(4L);
}
1
2
3
4

至此,最简单的crud到此结束,但是这种简单的增删改查在具体业务中用的很少,实际业务的逻辑要复杂的多。所以,条件构造器便应运而生。

# 条件构造器

条件构造器可以简单的理解为sql语句中你需要附加的条件。所以你不需要亲自编写sql语句也能完成一些复杂的sql查询。

# AbstractWrapper

QueryWrapper和UpdateWrapper的父类,该类中定义了许多条件构造的方法

一下列举一些常用的,更多请查看官方文档-条件构造器 (opens new window)

# eq

eq即equals,用于构建相等条件。

下面通过一个案例来进行解释,之后的将不再详细介绍。

@Test
public void testSelect() {
    QueryWrapper queryWrapper = new QueryWrapper();
    queryWrapper.eq("name","张三");
    TTest tTest1 = tTestMapper.selectOne(queryWrapper); 
    // TTest(id=1, name=张三)
}
1
2
3
4
5
6
7

可以看到,这里我们构建了一个name=张三的条件,然后让mapper通过该条件成功查询到了我们需要的数据。

实际上mapper中的很多方法都可以传入条件构造器,在idea中通过mapper.可以看到mapper中的所有方法,同时也可以看到每个方法需要的参数,并且每次方法都是见名知义的。

最后贴上eq的不同重载

eq(R column, Object val)
eq(boolean condition, R column, Object val)
1
2

可以看到有一个加了condition的重载方法,在wrapper类中很多方法都有这个参数,该参数表示是否生效此条件。

举个例子,你可以根据判断输入字符串是否为空来决定要不要构建该条件,写个伪代码

name = fromUserInput();
wrapper.eq(name.isNotEmpty(),"name",name)
1
2

通过这种设计,我们可以避免构建出一些糟糕的,并不需要的条件。

# allEq

表示全部eq(或个别isNull)

allEq(Map<R, V> params)
allEq(Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
1
2
3

params : key为数据库字段名,value为字段值 null2IsNull : 为true则在mapvaluenull时调用 isNull (opens new window) 方法,为false时则忽略valuenull

# ne

表示不等于 <>

ne(R column, Object val)
ne(boolean condition, R column, Object val)
1
2
# gt

表示大于,即 >

gt(R column, Object val)
gt(boolean condition, R column, Object val)
1
2

例如:gt("age", 18)--->age > 18

# ge

表示大于等于

ge(R column, Object val)
ge(boolean condition, R column, Object val)
1
2
# lt

表示小于

lt(R column, Object val)
lt(boolean condition, R column, Object val)
1
2
# le

表示小于等于

le(R column, Object val)
le(boolean condition, R column, Object val)
1
2
# between

表示两者之间

between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)
1
2
# in

字段 IN (value.get(0), value.get(1), ...)

in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)

in(R column, Object... values)
in(boolean condition, R column, Object... values)
1
2
3
4
5

例: in("age",{1,2,3})--->age in (1,2,3)

例: in("age", 1, 2, 3)--->age in (1,2,3)

# like

模糊查询

like(R column, Object val)
like(boolean condition, R column, Object val)
1
2

例: like("name", "王")--->`name like '%王%'

# groupBy

分组:GROUP BY 字段

groupBy(R... columns)
groupBy(boolean condition, R... columns)
1
2
# orderByAsc & orderByDesc

正 / 反排序

orderByAsc(R... columns)
orderByAsc(boolean condition, R... columns)
    
orderByDesc(R... columns)
orderByDesc(boolean condition, R... columns)
1
2
3
4
5
# having

HAVING ( sql语句 )

having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)
1
2

例: having("sum(age) > 10")--->having sum(age) > 10

例: having("sum(age) > {0}", 11)--->having sum(age) > 11

# and

AND 嵌套

and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
1
2

例: and(i -> i.eq("name", "李白").ne("status", "活着"))--->and (name = '李白' and status <> '活着')

# or

拼接 OR

or()
or(boolean condition)
1
2

例: eq("id",1).or().eq("name","老王")--->id = 1 or name = '老王'

注意事项:

主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)

# exists & notExists

拼接 EXISTS / NOT EXISTS ( sql语句 )

exists(String existsSql)
exists(boolean condition, String existsSql)

notExists(String notExistsSql)
notExists(boolean condition, String notExistsSql)
1
2
3
4
5

# QueryWrapper

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件 及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取

# select

设置查询字段

select(String... sqlSelect)
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
1
2
3

以上方法分为两类. 第二类方法为:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值! 这两类方法重复调用以最后一次为准

@Test
public void testSelect() {
    QueryWrapper queryWrapper = new QueryWrapper();
    queryWrapper.select("id");
    List list = tTestMapper.selectList(queryWrapper);
    // [TTest(id=1, name=null), TTest(id=2, name=null), TTest(id=3, name=null)]
}
1
2
3
4
5
6
7

# UpdateWrapper

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件 及 LambdaUpdateWrapper, 可以通过 new UpdateWrapper().lambda() 方法获取

# set

SQL SET 字段

set(String column, Object val)
set(boolean condition, String column, Object val)
1
2

例: set("name", "老李头")

# setSql

设置 SET 部分 SQL

setSql(String sql)
1

例: setSql("name = '老李头'")

# 代码生成器

AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。

说到代码生成器,应该不少人用过mybatis generator (opens new window)

作为mybatis的增加版,代码生成器自然也是必不可少的。mybatis plus的代码生成器可谓十分优雅,无须繁琐的xml配置文件,不需要maven插件,只需要一个java类,运行即可!

# 添加依赖

MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖:

添加代码生成器依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.3.2</version>
</dependency>
1
2
3
4
5

添加 模板引擎 依赖,MyBatis-Plus 支持 Velocity(默认)、Freemarker、Beetl,用户可以选择自己熟悉的模板引擎,如果都不满足您的要求,可以采用自定义模板引擎。

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>
1
2
3
4
5

注意!如果您选择了非默认引擎,需要在 AutoGenerator 中 设置模板引擎。

AutoGenerator generator = new AutoGenerator();

// set freemarker engine
generator.setTemplateEngine(new FreemarkerTemplateEngine());

// set beetl engine
generator.setTemplateEngine(new BeetlTemplateEngine());

// set custom engine (reference class is your custom engine class)
generator.setTemplateEngine(new CustomTemplateEngine());

// other config
...
1
2
3
4
5
6
7
8
9
10
11
12
13

# 编写配置

MyBatis-Plus 的代码生成器提供了大量的自定义参数供用户选择,能够满足绝大部分人的使用需求。

这里新建config包用来存放配置类。

删掉之前测试新建的entity和mapper包以及包中的类,同时注释掉单元测试中对实体及mapper的引用。

image-20210323203254418

以下是配置文件,内容不多,并且都加上了注释,相信读者可以根据自己的环境自行修改。

  • CodeGenerator.java
package xyz.ayay.mpdemo.config;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
import com.baomidou.mybatisplus.generator.config.converts.MySqlTypeConvert;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.config.rules.FileType;
import com.baomidou.mybatisplus.generator.config.rules.IColumnType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

    public static String scanner(String tip) {
        // 读取表名称
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("AlightYoung");
        // 生成后是否打开资源管理器
        gc.setOpen(false);
        // 实体属性 Swagger2 注解
        gc.setSwagger2(false);
        // 覆盖已有文件,这里先不设置覆写,后面通过cfg.setFileCreate指定覆盖
        gc.setFileOverride(false);
        //去掉Service接口的首字母I
        gc.setServiceName("%sService");
        //主键策略
        gc.setIdType(IdType.ASSIGN_ID);
        // 定义生成的实体类中日期类型
        gc.setDateType(DateType.ONLY_DATE);

        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/db_test?&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        // 数据库类型映射
        dsc.setTypeConvert(new MySqlTypeConvert() {
            @Override
            public IColumnType processTypeConvert(GlobalConfig globalConfig, String fieldType) {
                //将数据库中datetime转换成date
                if (fieldType.toLowerCase().contains("datetime")) {
                    return DbColumnType.DATE;
                }
                return (DbColumnType) super.processTypeConvert(globalConfig, fieldType);
            }
        });
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
        // pc.setModuleName(scanner("模块名"));
        pc.setParent("xyz.ayay.mpdemo");
        pc.setController("controller");
        pc.setEntity("entity");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        // String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /**
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        // 设置覆盖哪些文件
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                //如果是Entity则直接返回true表示写文件
                if (fileType == FileType.ENTITY) {
                    return true;
                }
                //否则先判断文件是否存在
                File file = new File(filePath);
                boolean exist = file.exists();
                if (!exist) {
                    file.getParentFile().mkdirs();
                }
                //文件不存在或者全局配置的fileOverride为true才写文件
                return !exist || configBuilder.getGlobalConfig().isFileOverride();
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // 不生成controller
        templateConfig.setController("");
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        // 命名采用下划线转驼峰
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        //生成代码时去掉表前缀
        strategy.setTablePrefix(pc.getModuleName() + "_");
        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        // 实体类添加lombok
        strategy.setEntityLombokModel(true);
        // RestController风格
        strategy.setRestControllerStyle(true);
        // url中驼峰转连字符
        strategy.setControllerMappingHyphenStyle(true);
        // 公共父类
        // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
        // strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        mpg.setStrategy(strategy);
        // 不设置 使用默认模板引擎VelocityTemplate
        // mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

当配置文件改完之后,右键执行:控制台出现

请输入表名,多个英文逗号分割:
t_test
1
2

回车,等待奇迹!

image-20210323203600341

到这儿,我们对代码生成器已经有了一定的认识。在实际工作场景中,表的数量基本都在上百张以上,虽然可能配置的类也相对复杂一些,但和编写几百个实体类mapper、service相比,无疑让我们减轻了不少负担。

# 自定义模板引擎

请继承类 com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine

更多详见官方文档-自定义模板引擎 (opens new window)

# 分页插件

分页在项目中的应用十分广泛。如果是在不使用分页插件的情况下写分页的话,效率必然大打折扣(对数据库底层不太了解的话,效率可能会很低)。所以,一个好用的分页插件无疑可以大幅提升开发效率(使开发人员专注于业务)。

在我之前的博客中已经介绍过了一种分页的实现方式,但是当时对分页的理解尚浅(现在也浅),写的也比较简单。不过感兴趣的还是可以看看。 ---- 分页的实现 (opens new window)

# 配置文件

config包下新建配置类 MybatisPlusConfig.java

这里需要注意新旧版本可能导致配置文件有很大的变化,当你发现配置不成功时(一般同版本不太可能出现问题),请考虑查询官方文档---分页实现 (opens new window)

package xyz.ayay.mpdemo.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {

    /**如果你是较新版的mybatis plus,请参考https://baomidou.com/guide/page.html,这个配置不适用于较新版本*/
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
         paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 测试代码

@Test
void testPagination() {
    // 每页两条记录,取第三页
    Page<TTest> page = new Page<>(1, 2);

    // page.addOrder(OrderItem.desc("id"));
    // Page<TTest> resultPage = tTestMapper.selectPage(page, null);
    // 使用上面的方法也能按照id进行倒排序
    Page<TTest> resultPage = tTestMapper.selectPage(page, Wrappers.<TTest>lambdaQuery().orderByDesc(TTest::getId));

    // 获取当前页的数据记录
    System.err.println(resultPage.getRecords()); // [TTest(id=6, name=王八), TTest(id=5, name=田七)]
    // 当前是第几页
    System.err.println(resultPage.getCurrent()); // 1
    // 总页数
    System.err.println(resultPage.getPages());   // 3
    // 每页的数据量
    System.err.println(resultPage.getSize());    // 2
    // 总共的记录数量
    System.err.println(resultPage.getTotal());   // 6
    // 这里并没有设置orders,需要的话则是在page条件中设置,lambdaQuery设置的排序这里查询不到
    System.err.println(resultPage.getOrders());  // []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这里我们就完成了一个基本的分页查询,似乎还是不用编写SQL

但在某些场景中,不管出于何种原因,我们仍然希望编写xml来进行自定义分页的话,mybatis plus也同样是支持的。

# XML 自定义分页

其实对mybatis熟悉的话,应该十分清楚该怎么在mapper中自定义sql,但是即使是自定义sql,也不需要关注分页的sql实现,这里提供一个简单的案例来说明

安利下MybatisX (opens new window)插件,不清楚的话点进去看一下就知道了。

安装完成之后,因为在上面的代码生成器中环节中,我们已经有了我们的mapper接口以及xml文件。

所以我们直接在mapper接口中添加一个分页方法

/**
     * <p>
     * 查询 : 根据name模糊查询用户列表,分页显示
     * </p>
     *
     * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象)
     * @param name name
     * @return 分页对象
     */
IPage<TTest> selectPageVo(Page<TTest> page, String name);
1
2
3
4
5
6
7
8
9
10

写完之后可以看到出现警告,hover即可发现generate statement,点击。对应的xml语句模板就出来了,然后点击左边的【红色小鸟】跳转过去。

编写一个简单的列表查询即可,无需实现分页逻辑(看上面接口的注释,mapper接口中方法只要传入了page参数,必须第一个,就可以 自 动 分 页!)

<select id="selectPageVo" resultType="xyz.ayay.mpdemo.entity.TTest">
    SELECT id,name FROM t_test where 1=1
    <if test="name!=null and name != ''">
        and name like concat('%',#{name},'%')
    </if>
</select>
1
2
3
4
5
6

下面编写测试类检查一下

@Test
void testXMLPagination() {
    // 不进行 count sql 优化,解决 MP 无法自动优化 SQL 问题,这时候你需要自己查询 count 部分
    // page.setOptimizeCountSql(false);
    // 当 total 为小于 0 或者设置 setSearchCount(false) 分页插件不会进行 count 查询
    // 要点!! 分页返回的对象与传入的对象是同一个,所以不用另外新建变量去接收
    Page<TTest> page = new Page<>(2,3);
    page.addOrder(OrderItem.asc("id"));
    tTestMapper.selectPageVo(page, null);
    System.err.println(page.getRecords()); // [TTest(id=4, name=赵六), TTest(id=5, name=田七), TTest(id=6, name=王八)]
    System.err.println(page.getCurrent()); // 2
    System.err.println(page.getPages());   // 2
    System.err.println(page.getSize());    // 3
    System.err.println(page.getTotal());   // 6
    System.err.println(page.getOrders());  // [OrderItem(column=id, asc=true)]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

至此,分页部分便告一段落,好处此刻已无需多言了。

# 主键&自动填充

# 自动生成主键

在之前的insert中,我们为对象设置了一个id,但其实mybatis plus可以自动生成id,无论是数字还是字符串类型,下面举个例子。

首先在实体类的id字段上加上注解

@TableId(value = "id",type = IdType.ASSIGN_ID)
private Long id;
1
2

然后编写测试类插入一条记录

@Test
void testAutoId() {
    TTest test = new TTest();
    test.setName("auto id row");
    tTestMapper.insert(test);
}
1
2
3
4
5
6

可以发现这里我们没有设置id,再看看数据库

id name
1374718566774980610 auto id row

可以看到记录被成功插入,并且生成了一长串数字作为id。

# 生成策略

可以看到,上面的注解指定了IdType,事实上,mybatis plus内置了几种常见类型的IdType,如果你对内置的不满意,也可以考虑自定义生成。下面简单介绍一个IdType。

IdType 描述
AUTO 使用自增方式生成,MySQL主键需设置Auto Increment
NONE 不自动生成,跟随全局默认INPUT
INPUT 用户在插入前自行设置
ASSIGN_ID 自动分配,雪花算法,Integer Long String类型均可
ASSIGN_UUID 自动分配,UUID算法,仅String类型
ID_WORKER 废弃,类似ASSIGN_ID的Integer和Long类型
ID_WORKER_STR 废弃,类似ASSIGN_ID的String类型
UUID 废弃,类似ASSIGN_UUID

# 自定义ID生成

首先,idtype需要设置为ASSIGN_ID或者ASSIGN_UUID,然后实现自定义id生成器,即编写一个类,实现IdentifierGenerator接口,同时标记注解 @Component

举个例子

@Component
public class CustomIdGenerator implements IdentifierGenerator {
    @Override
    public Long nextId(Object entity) {
        //可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
        String bizKey = entity.getClass().getName();
        //根据bizKey调用分布式ID生成
        long id = new Date().hashCode() + bizKey.hashCode();
        //返回生成的id值即可.
        return id;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 自动填充

考虑到使用场景,这里我们在表中添加两个字段

create_time & update_time,顺便把主键改成递增,之前建表忘了加。

然后使用之前的代码生成器重新生成实体类

新建Handler包,并创建一个类继承MetaObjectHandler

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        // 新增时需自动填充创建及修改时间
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 修改时仅自动填充修改时间
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这里需要注意,不同版本的api可能会发生改变,详细参考官方文档,传送门 (opens new window)

注意事项:

  • 填充原理是直接给entity的属性设置值!!!
  • 注解则是指定该属性在对应情况下必有值,如果无值则入库会是null
  • MetaObjectHandler提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null则不填充
  • 字段必须声明TableField注解,属性fill选择对应策略,该声明告知Mybatis-Plus需要预留注入SQL字段
  • 填充处理器MyMetaObjectHandler在 Spring Boot 中需要声明@Component@Bean注入
  • 要想根据注解FieldFill.xxx字段名以及字段类型来区分必须使用父类的strictInsertFill或者strictUpdateFill方法
  • 不需要根据任何来区分可以使用父类的fillStrategy方法

接下来对实体类字段添加注解

@Data
@EqualsAndHashCode(callSuper = false)
public class TTest implements Serializable {

    private static final long serialVersionUID=1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String name;

    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里解释一下FieldFill的各个值

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

之后使用insert+update进行测试即可,有问题就仔细检查handler有没有错误。

# 逻辑删除

简单来说,正常通过delete操作的数据将真正从表中删除,但有些场景下我们希望记录

被删除后,表中仍然有记录,通过添加标志位,来判断记录是否被删除。

照常编写一个简单的案例

  1. 首先在数据库表中添加一个is_deleted字段,类型tiny_int即可(只会用到0和1)

  2. 在springboot的配置文件application.yml添加以下配置

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: isDeleted  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置实体类字段上的@TableLogic注解)
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    
    1
    2
    3
    4
    5
    6
  3. 使用代码生成器重新生成实体类,记得加上之前两个@TableField已实现自动填充(嫌麻烦直接在后面手动加上isDeleted属性也行。另外我之前在代码生成器环节配置了实体类存在也进行覆盖,不想覆盖的自己改回来)

  4. 编写删除查询进行测试即可

这里测试逻辑测试发现记录确实没有被删除,但是isDeleted字段也没有改变,这是因为之前的记录的isDeleted为null,而在加入逻辑删除之后会把查询及更新的语句都附加一个条件, isDeleted = 0,所以建议在数据库中将isDeleted字段的默认值设置为0。下面是官方的一些说明及问题。

说明:

只对自动注入的sql起效:

  • 插入: 不作限制
  • 查找: 追加where条件过滤掉已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
  • 更新: 追加where条件防止更新到已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
  • 删除: 转变为 更新

例如:

  • 删除: update user set deleted=1 where id = 1 and deleted=0
  • 查找: select id,name,deleted from user where deleted=0

字段类型支持说明:

  • 支持所有数据类型(推荐使用 Integer,Boolean,LocalDateTime)
  • 如果数据库字段使用datetime,逻辑未删除值和已删除值支持配置为字符串null,另一个值支持配置为函数来获取值如now()

附录:

  • 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
  • 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。

image-20210325210627121

# 乐观锁

# 乐观锁简介

乐观锁是并发控制中比较常见的的锁,它采取了更加宽松的加锁机制用于提高系统的性能。(乐观锁机制避免了长事务中的数据库加锁开销)最常见的是采用Version进行对数据进行记录。下面简单介绍一下它的实现方式。

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

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

再补充一个例子加以理解。

在一个金融系统中,操作员A和操作员B都要对客户的金额进行修改,所以都获取了客户当前金额100元(同时获取当前version,假设为1

  • 操作员A扣除其40元,并将剩余金额60元进行返回

  • 操作员A扣除其20元,并将剩余金额80元进行返回

假设操作员A先执行了更新,返回了客户的余额60,并设置了version=2

然后操作员B在进行更新的时候发现version此时为2,和之前获取的不一致,所以会取消当前更新。(假设没有version的判断,操作员B将余额80进行返回,那么客户的余额反而更多了,这显然是不合理的

# 乐观锁的实现

Mybatis Plus中使用了OptimisticLockerInnerInterceptor来实现乐观锁。

使用方法

  1. 在数据库中添加version字段,类型为int

  2. 类似于分页插件,在mybatisPlusConfig添加一个bean,返回OptimisticLockerInnerInterceptor。

    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        OptimisticLockerInterceptor optimisticLockerInterceptor = new OptimisticLockerInterceptor();
        return optimisticLockerInterceptor;
    }
    
    1
    2
    3
    4
    5
  3. 在实体类添加属性,并加上以下注解,尽管只使用了FieldFill.INSERT,但是更新时仍会自动填充。

    @Version
    @TableField(fill = FieldFill.INSERT)
    private Integer version;
    
    1
    2
    3
  4. 在MyMetaObjectHandler中实现version字段的自动填充,给version一个初始值1。

    @Override
    public void insertFill(MetaObject metaObject) {
        // 新增时需自动填充创建及修改时间
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "version", Integer.class, 1);
    }
    
    1
    2
    3
    4
    5
    6
    7
  5. 进行新增以及修改测试,注意修改时必须先查询,只进行修改不查询乐观锁将不再生效

    @Test
    public void testInsert() {
        TTest testObj = new TTest();
        testObj.setName("移动");
        tTestMapper.insert(testObj);
    }
    
    @Test
    public void testUpdate() {
        /**
        * 这种直接的修改乐观锁不会生效(version不会改变)
        * TTest test = new TTest();
        * test.setName("移动update");
        * tTestMapper.update(test, Wrappers.<TTest>lambdaQuery().eq(TTest::getName, "移动"));
        **/
        // 先查询,后修改,乐观锁生效
        TTest test = tTestMapper.selectOne(Wrappers.<TTest>lambdaQuery().eq(TTest::getName, "移动"));
        test.setName("移动update");
        tTestMapper.updateById(test);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

说明:

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

# 最后

Mybatis Plus至此便告一段落,更多细节请参考官方文档 (opens new window)u1s1官方文档的细节还有待完善,小声bb