MyBatis-plus

cccs7 Lv5

MyBatis-Plus

**2023/01/01 - **

MyBatis-Plus 开发文档 https://baomidou.com/


MyBatis-Plus 简介

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


特性


  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持数据库


任何能使用 MyBatis 进行 CRUD ,并且支持标准 SQL 的数据库,具体支持如下。如果不在下列表查看分页部分教程 PR 所用的支持


  • MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库

架构结构


framework

代码文档及文档地址


官方地址: http://mp.baomidou.com

代码发布地址:

Github: https://github.com/baomidou/mybatis-plus

Gitee: https://gitee.com/baomidou/mybatis-plus

文档发布地址: https://baomidou.com/pages/24112f


MyBatis-Plus 入门案例

开发环境


IDE :IDEA 2021.03

JDK : 17

构建工具 : maven 3.8.1

MySQL : 8.0.31

MyBatis-Plus :3.5.4

系统 : win 10 x64


创建数据库及表


创建表

1
2
3
4
5
CREATE DATABASE `mybatis_plus` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
use `mybatis_plus`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT '主键ID', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加数据

1
2
3
4
5
6
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

创建 SpringBoot 工程


初始化工程

使用 spring initializr


导入依赖

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
<dependencies>
<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>

<!--MyBatis-plus启动器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

<!--lombok用于简化实体类开发-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

lombok 简化开发


编写代码


配置 application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
# 配置数据源信息
datasource:
# 配置数据源类型
type:
# 配置连接数据库信息
driver-class-name: com.mysql.cj.jdbc.Driver
# 我的数据库是8.0.27 5版本的可以使用 jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: root
password: C020611.


配置注意事项
  • 配置驱动类 driver-class-name
    • springboot 2.0(内置 jdbc5驱动),驱动类使用
      • driver-class-name: com.mysql.jdbc.Driver
    • springboot 2.1 及以上(内置 jdbc8 驱动),驱动类使用
      • driver-class-name: com.mysql.cj.jdbc.Driver

否则运行测试用例的时候会有 WARN 信息


连接地址 URL
  • MySQL5.7版本的url:
    • jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
  • MySQL8.0版本的url:
    • jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false

否则 运行测试用例报告如下错误

java.sql.SQLException: The server time zone value 'ÖÐ1ú±ê×1⁄4ʱ1⁄4ä' is unrecognized or represents more


启动类

在 SpringBoot 启动类 中添加 @MapperScan 注解,扫描 mapper 包

1
2
3
4
5
6
7
8
9
@MapperScan("com.cs7eric.mybatisplus.mapper")
@SpringBootApplication
public class MybatisPlusApplication {

public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}

}

添加实体

1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

private Long id;
private String name;
private Integer age;
private String email;
}

使用了 Lombok 注解


添加 mapper

BaseMapper 是 MyBatis-Plus 提供的模板 mapper,其中包含了基本的 CRUD 方法,泛型为操作的实体类类型


1
2
public interface UserMapper extends BaseMapper<User> {
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
class MybatisplusApplicationTests {

@Autowired
private UserMapper userMapper;

@Test
public void testSelectList() {
// 通过条件构造器查询一个List集合,若没有条件,则可以设置null为参数
List<User> list = userMapper.selectList(null);
list.forEach(System.out::println);
}
}

注意:

IDEA 在 UserMapper 处报错,因为找不到 注入的对象,因为类是动态创建的,但是程序可以正确执行,为了避免报错,可以 mapper 接口处添加 @Repository


添加日志

在 application.yaml 中配置日志输出

1
2
3
4
# 配置日志输出
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

MyBatis-Plus 使用


基本 CRUD


BaseMapper

MyBatis-Plus 中的基本CRUD 在内置的 BaseMapper 中已经得到了 实现,可以直接使用,接口如下:

1
public interface BaseMapper<T> extends Mapper<T> {

这个 Mapper 支持 泛型,泛型就是操作的 类型


Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得 CRUD 功能

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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.mybatisplus.core.mapper;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import org.apache.ibatis.annotations.Param;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/*

:`
.:,
:::,,.
:: `::::::
::` `,:,` .:`
`:: `::::::::.:` `:';,`
::::, .:::` `@++++++++:
`` :::` @+++++++++++#
:::, #++++++++++++++`
,: `::::::;'##++++++++++
.@#@;` ::::::::::::::::::::;
#@####@, :::::::::::::::+#;::.
@@######+@:::::::::::::. #@:;
, @@########':::::::::::: .#''':`
;##@@@+:##########@::::::::::: @#;.,:.
#@@@######++++#####'::::::::: .##+,:#`
@@@@@#####+++++'#####+::::::::` ,`::@#:`
`@@@@#####++++++'#####+#':::::::::::@.
@@@@######+++++''#######+##';::::;':,`
@@@@#####+++++'''#######++++++++++`
#@@#####++++++''########++++++++'
`#@######+++++''+########+++++++;
`@@#####+++++''##########++++++,
@@######+++++'##########+++++#`
@@@@#####+++++############++++;
;#@@@@@####++++##############+++,
@@@@@@@@@@@###@###############++'
@#@@@@@@@@@@@@###################+:
`@#@@@@@@@@@@@@@@###################'`
:@#@@@@@@@@@@@@@@@@@##################,
,@@@@@@@@@@@@@@@@@@@@################;
,#@@@@@@@@@@@@@@@@@@@##############+`
.#@@@@@@@@@@@@@@@@@@#############@,
@@@@@@@@@@@@@@@@@@@###########@,
:#@@@@@@@@@@@@@@@@##########@,
`##@@@@@@@@@@@@@@@########+,
`+@@@@@@@@@@@@@@@#####@:`
`:@@@@@@@@@@@@@@##@;.
`,'@@@@##@@@+;,`
``...``

_ _ /_ _ _/_. ____ / _
/ / //_//_//_|/ /_\ /_///_/_\ Talk is cheap. Show me the code.
_/ /
*/

/**
* Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
* <p>这个 Mapper 支持 id 泛型</p>
*
* @author hubin
* @since 2016-01-23
*/
public interface BaseMapper<T> extends Mapper<T> {

/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);

/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);

/**
* 根据实体(ID)删除
*
* @param entity 实体对象
* @since 3.4.4
*/
int deleteById(T entity);

/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

/**
* 根据 entity 条件,删除记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 删除(根据ID或实体 批量删除)
*
* @param idList 主键ID列表或实体列表(不能为 null 以及 empty)
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<?> idList);

/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);

/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);

/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T selectById(Serializable id);

/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

/**
* 查询(根据 columnMap 条件)
*
* @param columnMap 表字段 map 对象
*/
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

/**
* 根据 entity 条件,查询一条记录
* <p>查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常</p>
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) {
List<T> ts = this.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(ts)) {
if (ts.size() != 1) {
throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records");
}
return ts.get(0);
}
return null;
}

/**
* 根据 Wrapper 条件,判断是否存在记录
*
* @param queryWrapper 实体对象封装操作类
* @return
*/
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0;
}

/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 根据 Wrapper 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 根据 Wrapper 条件,查询全部记录
* <p>注意: 只返回第一个字段的值</p>
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
<P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
*/
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}

插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
@DisplayName("testInsert")
public void testInsert(){

//实现 插入功能
// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
User user = new User();
user.setName("张三");
user.setAge(12);
user.setEmail("zhangsan@atguigu.com");
int result = userMapper.insert(user);
System.out.println(result);
System.out.println("id = " + user.getId());
}

最终获得的 ID 为 1610518676744368130

这是因为 MyBatis-Plus 在实现插入数据时,会默认基于雪花算法的策略生成 ID


删除

根据 ID 查询 用户信息
1
2
3
4
5
6
7
8
9
10
@Test
@DisplayName("testDeleteById")
public void testDeleteById(){

//通过 ID 删除 用户信息
// DELETE FROM user WHERE id = ?
int result = userMapper.deleteById(1610518676744368130L);
System.out.println(result);

}

根据 多个 ID 查询 多个用户信息
1
2
3
4
5
6
7
8
9
10
@Test
@DisplayName("testDeleteBatchIds")
public void testDeleteBatchIds(){

// 通过 多个 ID 批量删除
// DELETE FROM user WHERE id IN(?,?,?)
List<Long> list = Arrays.asList(1L, 2L, 3L);
int result = userMapper.deleteBatchIds(list);
System.out.println(result);
}

根据 map 条件删除记录
1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("testDeleteByMap")
public void testDeleteByMap(){

//根据 map 集合中所设置的条件 删除 记录
// DELETE FROM user WHERE name = ? AND age = ?
Map<String,Object> map = new HashMap<>();
map.put("age",23);
map.put("name","张三");
int result = userMapper.deleteByMap(map);
System.out.println(result);
}

修改

1
2
3
4
5
6
7
8
9
10
11
12
/** 测试BaseMapper的 修改功能  **/
@Test
public void testUpdate() {
// 修改用户信息
// UPDATE user SET name=?, email=? WHERE id=?
User user = new User();
user.setIdƔL);
user.setName("李四");
user.setEmail("lisi@atguigu.com");
userMapper.updateById(user);
System.out.println("user = " + user);
}

查询

根据 ID 查询用户信息
1
2
3
4
5
6
7
@Test
@DisplayName("testSelectById")
public void testSelectById() {

User user = userMapper.selectById(3L);
System.out.println(user);
}

根据 多个ID 查询 多个用户信息
1
2
3
4
5
6
7
8
@Test 
public void testSelectBatchIds(){
// 根据多个id查询多个用户信息
// SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? )
List<Long> list = Arrays.asListƑL, 2L, 3L);
List<User> users = userMapper.selectBatchIds(list);
users.forEach(System.out::println);
}

通过 map 条件查询 用户信息
1
2
3
4
5
6
7
8
9
10
@Test
public void testSelectByMap(){
// 根据map集合中的条件查询用户信息
// SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
Map<String, Object> map = new HashMap<>();
map.put("name", "Jack");
map.put("age", 18);
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}

查询所有数据
1
2
3
4
5
6
7
@Test
public void testSelectList(){
// 查询所有数据
// SELECT id,name,age,email FROM user
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}

通过测试 BaseMapper 中的方法,大多方法中都有 Wrapper 类型的形参,此为 条件构造器,可针对于 SQL 语句 设置不同的条件,若没有条件,则可以为 该形参赋值为 null,即查询(删除、修改)所有数据


自定义功能
UserMapper.java
1
2
/** 根据id查询用户信息为map集合 **/
Map<String,Object> selectMapById(Long id);

UserMapper.xml
1
2
3
4
<!--Map<String,Object> selectMapById(Long id);-->
<select id="selectMapById" resultType="map">
select id,name,age,email from t_user where id = #{id}
</select>

test
1
2
3
4
5
@Test
public void testConsumer() {
Map<String, Object> map = userMapper.selectMapById(1L);
System.out.println("map = " + map);
}

通用 Service

通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove

list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,

泛型 T 为任意实体对象

建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承

Mybatis-Plus 提供的基类

官网地址


IService

MyBatis-Plus 中有一个 接口 IService 和 其 实现类 ServiceImpl,封装了常见的业务层逻辑,详情查看源码 IServiceServiceImpl


创建 Service 接口 和 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 接口  UserService继承IService模板提供的基础功能
public interface UserService extends IService<User> {

}
// 实现类
/*
ServiceImpl实现了IService,提供了IService中基础功能的实现
若ServiceImpl无法满足业务需求,则可以使用自定的UserService定义方法,
并在实现类中实现
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

}

Service 接口

接口 UserService 继承 IService 模板提供的基础功能


实现类

ServiceImpl实现了IService,提供了IService中基础功能的实现。若ServiceImpl无法满足业务需求,则可以使用自定的UserService定义方法,并在实现类中实现


测试查询记录
1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private UserService userService;


/** 查询总记录数 **/
@Test
public void testGetCount() {
// 查询记录数
// SELECT COUNT( * ) FROM user
long count = userService.count();
System.out.println("总记录数 = " + count);
}

测试批量插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 批量添加的功能 **/
@Test
public void testInsertMore() {
// SQL长度有限制,海量数据插入单条SQL无法实行,
// 因此MP将批量插入放在了通用Service中实现,而不是通用Mapper
// 批量添加
// INSERT INTO user ( id, name, age ) VALUES ( ?, ?, ? )
List<User> users = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
User user = new User();
user.setName("lxg" + i);
user.setAge(20 + i);
users.add(user);
}
boolean b = userService.saveBatch(users);
System.out.println(b);
}

常见注解


@TableName

经过以上测试,在使用 MyBatis-Plus 实现基本 CRUD 时,我们并没有指定要 操作的表,只是在 Mapper 接口 继承 BaseMapper 时,设置了 泛型 User,而操作 的 表为 user 表

由此 得出结论,MyBatis-Plus 在 确定操作的表时,由 BaseMapper 的泛型决定,即 实体类型决定,且默认操作的表名和实体类型 的类名一致


问题

若 实体类类型的类名和要操作的表的表名不一致,会抛出异常

Cause: com.mysql.jdbc.exceptions.jdbc8.MySQLSyntaxErrorException : Table ‘xxx’ doesn’t exist


通过 @TableName 解决

在 实体类 类型上添加 @TableName("t_user") ,标识实体类对应的表,即可成功执行 SQL 语句


通过全局配置解决问题

在开发过程中,会遇到 实体类所对应的表有固定前缀,例如 t_

此时,可以使用 MyBatis-Plus 提供的全局配置,为实体类所对应的表名设置默认的前缀,那么就不需要在每个实体类上通过 @TableName 标识 实体类对应的表


1
2
3
4
5
6
7
8
9
mybatis-plus:
configuration:
# 配置MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 设置MyBatis-Plus的全局配置
global-config:
db-config:
# 设置实体类所对应的表的统一前缀
table-prefix: t_

@TableId

经过以上的测试 ,MyBatis-Plus 在实现 CRUD 时,会默认将 ID 作为 主键列 ,并在插入数据时,**默认 基于雪花算法的策略生成 ID **


问题

若 实体类和表中表示主键的 不是 ID,而是其他字段,例如 uid,MyBatis-Plus 会自动识别 uid 为主键列吗

-> 程序 抛出异常 Filed uid doesn’t have a default value , 说明 mp 没有将 uid 作为主键赋值


通过 @TableId 解决 问题

在 实体类中 uid 属性上通过 @TableId 将其作为 主键,即可成功执行 SQL 语句


@TableId 的 value 属性

若实体类中主键对应的属性为id,而表中表示主键的字段为uid,此时若只在属性id上添加注解

@TableId,则抛出异常Unknown column ‘id’ in ‘field list’,即MyBatis-Plus仍然会将id作为表的

主键操作,而表中表示主键的是字段uid

此时需要通过**@TableId注解的value属性,指定表中的主键字段**,@TableId(“uid”)或

@TableId(value=”uid”)


image.png


@TableId 的 type 属性

type 属性 用来定义 主键策略


常用的主键策略
image.png
配置全局主键策略
1
2
3
4
5
6
7
8
9
10
11
# 加入日志功能
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 设置MyBatis-Plus的全局配置
global-config:
db-config:
# 设置实体类所对应的表的统一前缀
table-prefix: t_
# 设置统一的主键生成策略
id-type: auto
雪花算法
背景

需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量

数据库的扩展方式主要包括:业务分库、主从复制,数据库分表。


数据库分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务

继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,

如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进

行拆分。

单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:

image.png
  • 垂直分表

    • 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。

      例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用

      户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展

      示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外

      一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。

  • 水平分表

    • 水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以

      作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000

      万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。

      但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性

      能瓶颈或者隐患。

      水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处


主键自增

①以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,

1000000 ~ 1999999 放到表2中,以此类推。

复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会

导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适

的分段大小。

优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,

只需要增加新的表就可以了,原有的数据不需要动。

缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而

另外一个分段实际存储的数据量有 1000 万条。


取模

①同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来

表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号

为 6 的子表中。

复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。

优点:表分布比较均匀。

缺点:扩充新的表很麻烦,所有数据都要重分布。


雪花算法

雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的

主键的有序性。

核心思想:

长度共64bit(一个long型)。

首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负

数是1,所以id一般是正数,最高位是0。

41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。

10bit作为机器的ID(

5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。

12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。

img

优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。


@TableField

通过以上的测试,我们可以发现,MyBatis-Plus 在执行 SQL 语句时,要保证 实体类中的属性和表中的字段名一致

如果 实体类中的属性名和字段名属性不一致的情况,会出现什么情况


情况 1

若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格

例如实体类属性userName,表中字段user_name

此时 MyBatis-Plus 会自动将下划线命名风格转化为 驼峰命名规则

相当于在MyBatis中配置


情况 2

若实体类中的属性和表中的字段不满足情况1


例如实体类属性name,表中字段username

此时需要在实体类属性上使用@TableField(“username”) 设置属性对应的字段名


@TableLogic

逻辑删除
  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库

中仍旧能看到此条数据记录

  • 使用场景:可以进行数据恢复

实现逻辑删除
step 1

数据库中创建 逻辑删除状态列,设置默认值为 0

image.png
step 2

实体类中添加 逻辑删除属性

image.png
step 3

测试删除功能,真正执行的是修改

UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0

测试查询功能,被逻辑删除的数据默认不会被查询

SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0


条件构造器 和 常用 接口


wrapper 介绍

image.png


Wrapper : 条件构造抽象类,最顶端父类

  • AbstractWrapper : 用于 查询条件封装,生成 SQL 的 where 条件
    • QueryWrapper : 查询条件封装
    • UpdateWrapper : Update 条件封装
    • AbstractLambdaWrapper : 使用 Lambda 语法
      • LambdaQueryWrapper : 用于 使用 Lambda 语法使用 的查询 Wrapper
      • LambdaUpdateWrapper : Lambda 更新封装Wrapper

QueryWrapper

组装查询条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 查询用户包含a,年龄在20到30之间,邮箱信息不为null的用户信息 **/
@Test
public void test01() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 AND (user_name LIKE ?
AND age BETWEEN ? AND ? AND email IS NOT NULL)
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("user_name", "a")
.between("age", 20, 30)
.isNotNull("email");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

组装排序条件
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 组装排序条件 查询用户信息,按照年龄的降序排序,若年龄相同,则按照id升序排序 **/
@Test
public void test02() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 ORDER BY age DESC,uid ASC
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age")
.orderByAsc("uid");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

组装删除条件
1
2
3
4
5
6
7
8
9
10
11
/** 组装删除条件 删除邮箱地址为null的用户信息 **/
@Test
public void test03() {
/*
UPDATE t_user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NOT NULL)
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
int result = userMapper.delete(queryWrapper);
System.out.println("result = " + result);
}

条件的优先级
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
 /** 组装修改条件  将(年龄大于20并且用户名中包含有a)或邮箱为null的用户信息修改 **/
@Test
public void test04() {
/*
UPDATE t_user SET user_name=?, email=?
WHERE is_deleted=0 AND (age > ? AND user_name LIKE ? OR email IS NOT NULL)
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20)
.like("user_name", "a")
.or()
.isNotNull("email");
User user = new User();
user.setName("小明");
user.setEmail("test@atguigu.com");
int result = userMapper.update(user, queryWrapper);
System.out.println("result = " + result);
}

/** 条件优先级 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改 **/
@Test
public void test05() {
// lambda中条件优先级
/*
UPDATE t_user SET user_name=?, email=?
WHERE is_deleted=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL))
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("user_name", "a")
.and(i -> i.gt("age", 20).or().isNull("email"));
User user = new User();
user.setName("小红");
user.setEmail("test@atguigu.com");
int result = userMapper.update(user, queryWrapper);
System.out.println("result = " + result);
}

组装 select 子句
1
2
3
4
5
6
7
8
9
10
11
/** 组装select字句 查询用户名的用户名、年龄、邮箱信息 **/
@Test
public void test06() {
/*
SELECT user_name,age,email FROM t_user WHERE is_deleted=0
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("user_name", "age", "email");
List<Map<String, Object>> users = userMapper.selectMaps(queryWrapper);
users.forEach(System.out::println);
}

实现子查询
1
2
3
4
5
6
7
8
9
10
11
12
/** 组装子查询 查询id小于100的用户信息**/
@Test
public void test07() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 AND (uid IN (select uid from t_user where uid <= 100))
*/
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.inSql("uid", "select uid from t_user where uid <= 100");
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}

UpdateWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 使用UpdateWrapper实现修改功能 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改 **/
@Test
public void test08() {
/*
UPDATE t_user SET user_name=?,email=?
WHERE is_deleted=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL))
*/
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.like("user_name", "a")
.and(i -> i.gt("age", 20).or().isNull("email"));
updateWrapper.set("user_name", "小黑").set("email", "abc@atguigu.com");
int result = userMapper.update(null, updateWrapper);
System.out.println("result = " + result);
}


Condition

在真正开发的过程中,组装条件是常见的功能, 而这些条件来源于用户输入,是可选的,因此我们在组装这些条件的时候,必须要先判断用户是否选择了这些条件, 若选择则需要组装该条件,若没有选择则一定不能组装,以免影响 SQL 执行的结果

思路一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 模拟开发中组装条件的情况  **/
@Test
public void test09() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 AND (user_name LIKE ? AND age <= ?)
*/
String username = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
// isNotBlank判断某个字符串是否不为空字符串、不为null、不为空白符
queryWrapper.like("user_name", username);
}
if (ageBegin != null) {
queryWrapper.gt("age", ageBegin);
}
if (ageEnd != null) {
queryWrapper.le("age", ageEnd);
}
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}

思路二

上面的实现方案没有问题,但是代码比较复杂,我们可以使用带 condition 参数的重载方法构建查询条件, 简化代码的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 使用condition组装条件 **/
@Test
public void test10() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 AND (user_name LIKE ? AND age <= ?)
*/
String username = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(username), "user_name", username)
.gt(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}

LambdaQueryWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** LambdaQueryWrapper **/
@Test
public void test11() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 AND (user_name LIKE ? AND age <= ?)
*/
String username = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
//组装set子句
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//避免使用字符串表示字段,防止运行时错误
queryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)
.gt(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}

LambdaUpdateWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** LambdaUpdateWrapper **/
@Test
public void test12() {
/*
UPDATE t_user SET user_name=?,email=?
WHERE is_deleted=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL))
*/
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.like(User::getName, "a")
//lambda表达式内的逻辑优先运算
.and(i -> i.gt(User::getAge, 20).or().isNull(User::getEmail));
updateWrapper.set(User::getName, "小黑").set(User::getEmail, "abc@atguigu.com");
int result = userMapper.update(null, updateWrapper);
System.out.println("result = " + result);
}

插件


分页插件

MyBatis-Plus 自带分页插件,只要简单的配置即可实现分页功能


添加配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {

/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}

@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}

新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)


测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private ProductMapper productMapper;

/** MyBatis-Plus分页插件的配置和使用和分页相关数据的获取 **/
@Test
@DisplayName("PagePlugin")
public void testPage() {
/*
SELECT uid AS id,user_name AS name,age,email,is_deleted FROM t_user
WHERE is_deleted=0 LIMIT ?
*/
Page<User> page = new Page<>(1, 3);
userMapper.selectPage(page, null);
System.out.println( page.getRecords());
System.out.println("总页数:" + page.getPages());
System.out.println("总记录数" + page.getTotal());
System.out.println("是否有上一页" + page.hasNext());
System.out.println("是否有下一页:" + page.hasPrevious());
}

xml 自定义分页

UserMapper 中定义接口方法
1
2
3
4
5
6
/**
* 根据年龄查询用户列表,分页显示
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位 * @param age 年龄
* @return
*/
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);

UserMapper.xml 中编写 SQL
1
2
3
4
<!--Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);-->
<select id="selectPageVo" resultType="User">
select uid as `id`,user_name as `name`,age,email from t_user where age > #{age}
</select>

测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testSelectPageVo(){ //设置分页参数
Page<User> page = new Page<>(1, 5);
userMapper.selectPageVo(page, 20);
//获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:"+page.getCurrent());
System.out.println("每页显示的条数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有上一页:"+page.hasPrevious());
System.out.println("是否有下一页:"+page.hasNext());
}

乐观锁

场景

一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小

李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太

高,可能会影响销量。又通知小王,你把商品价格降低30元。


此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王

也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据

库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就

完全被小王的覆盖了。


现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1

万多


乐观锁和悲观锁

上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。

如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证

最终的价格是120元。


模拟修改冲突
数据库中增加商品表
1
2
3
4
5
6
CREATE TABLE t_product
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称', price INT(11) DEFAULT 0 COMMENT '价格',
VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id)
);

添加数据
1
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本', 100);

添加实体
1
2
3
4
5
6
7
8
9
10
11
package com.cs7eric.mybatisplus.entity;

import lombok.Data;

@Data
public class Product {
private Long id;
private String name;
private Integer price;
private Integer version;
}

添加 mapper
1
2
3
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}

测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testProduct01() {
// 1.小李查询商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询的商品价格:" + productLi.getPrice());

// 2.小王查询商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询的商品价格:" + productWang.getPrice());

// 3.小李商品价格+50
productLi.setPrice(productLi.getPrice() + 50);
productMapper.updateById(productLi);

// 4.小王将商品价格-30
productWang.setPrice(productWang.getPrice() - 30);
int result = productMapper.updateById(productWang);

// 5.老板查询商品价格
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询的商品价格:" + productBoss.getPrice());
}

乐观锁实现流程

数据库中添加 version 字段

取出记录时,获取当前的 version


1
SELECT id,`name`,price,`version` FROM product WHERE id=1

更新时,version + 1,如果 where 语句中的 version 版本不对,则更新失败

1
2
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND
`version`=1

MyBatis-Plus 实现乐观锁
修改实体类
1
2
3
4
5
6
7
8
9
10
@Data
public class Product {

private Long id;
private String name;
private Integer price;
@Version // 设置乐观锁版本号字段
private Integer version;

}

添加乐观锁插件配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@MapperScan("com.cs7eric.mybatisplus.mapper")
public class MyBatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {

MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//乐观锁插件
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}

测试修改冲突

小李查询商品信息:

SELECT id,name,price,version FROM t_product WHERE id=?

小王查询商品信息:

SELECT id,name,price,version FROM t_product WHERE id=?

小李修改商品价格,自动将version+1

UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters: 外星人笔记本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer)

小王修改商品价格,此时version已更新,条件不成立,修改失败

UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters: 外星人笔记本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer)

最终,小王修改失败,查询价格:150

SELECT id,name,price,version FROM t_product WHERE id=?


优化流程
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
@Test
public void testProduct01() {
// 1.小李查询商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询的商品价格:" + productLi.getPrice());

// 2.小王查询商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询的商品价格:" + productWang.getPrice());

// 3.小李商品价格+50
productLi.setPrice(productLi.getPrice() + 50);
productMapper.updateById(productLi);

// 4.小王将商品价格-30
productWang.setPrice(productWang.getPrice() - 30);
int result = productMapper.updateById(productWang);
if (result == 0) {
// 操作失败,重试
Product productNew = productMapper.selectById(1);
productNew.setPrice(productNew.getPrice() - 30);
productMapper.updateById(productNew);
}

// 5.老板查询商品价格
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询的商品价格:" + productBoss.getPrice());
}

通用枚举


表中有些字段值是固定的,例如 性别,此时我们可以使用 MyBatis-Plus 的通用枚举来实现


数据库表添加字段 sex

image-20230106201129905


创建通用枚举类型

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
package com.cs7eric.mybatisplus.enums;

import lombok.Getter;

/**
* 性别枚举
*
* @author cs7eric
* @date 2023/01/06
*/
@Getter
public enum SexEnum {

/**
* 男性
*/
MALE(1, "男"),
/**
* 女
*/
FEMALE(2, "女");


private final Integer sex;

private final String sexName;

SexEnum(Integer sex, String sexName) {
this.sex = sex;
this.sexName = sexName;
}
}


配置扫描通用枚举

type-enums-package: com.atguigu.mybatisplus.enums

该配置已弃用


测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
public class MyBatisPlusEnumTest {

@Autowired
private UserMapper userMapper;

@Test
public void test() {
User user = new User();
user.setName("admin");
user.setAge(33);
user.setSex(SexEnum.MALE);
int result = userMapper.insert(user);
System.out.println("result = " + result);
}

}

代码生成器


引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>

快速生成

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
package com.cs7eric.mybatisplus;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collections;

public class FastAutoGenerateTest {

public static void main(String[] args) {
// 设置我们需要创建在哪的路径
String path = "D://cache_repo//demo";
// 这里我是mysql8 5版本可以换成 jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
FastAutoGenerator.create("jdbc:mysql://localhost:3306/sql_repo?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false", "root", "C020611.")
.globalConfig(builder -> {
builder.author("cs7eric") // 设置作者
// .enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir(path); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.cs7eric") // 设置父包名
.moduleName("mybatisplus") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, path)); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
// builder.addInclude("t_user") // 设置需要生成的表名
// .addTablePrefix("t_", "c_"); // 设置过滤表前缀
}).templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker 引擎模板,默认的是Velocity引擎模板
.execute();
}

}

点击运行即可


多种数据源


适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等 目前我们就来模拟一个纯粹多库的一个场景,其他场景类似

场景说明:

我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将 mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例 分别获取用户数据与商品数据,如果获取到说明多库模拟成功


创建数据库

创建数据库 和 表

1
2
3
4
5
6
7
8
CREATE DATABASE `mybatis_plus_1` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
use `mybatis_plus_1`;
CREATE TABLE product
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称', price INT(11) DEFAULT 0 COMMENT '价格',
version INT(11) DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id)
);

添加测试数据

1
INSERT INTO product (id, NAME, price) VALUES (1, '外星人笔记本', 100);

引入依赖

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
<!--MyBatis-plus启动器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

<!--lombok用于简化实体类开发-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--多数据源依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>

配置多数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
datasource:
# 配置数据源信息 datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 'C020611.'
slave_1:
# 我的数据库是8.0.31 5版本的可以使用jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
url: jdbc:mysql://localhost:3306/mybatis_plus_1?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 'C020611.'

创建用户Service

1
public interface UserService extends IService<User> {}

1
2
3
@Service
@DS("master") // 指定所操作的数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}

创建商品service

1
public interface ProductService extends IService<Product> {}

1
2
3
@Service
@DS("slave_1") // 要操作的数据源
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class MybatisPlusDatasourceApplicationTests {

@Autowired
private UserService userService;

@Autowired
private ProductService productService;

@Test
public void test() {
// 测试
System.out.println(userService.getById(1L));
System.out.println(productService.getById(1L));
}

}

结果:

1、都能顺利获取对象,则测试成功

2、如果我们实现读写分离,将写操作方法加上主库数据源,读操作方法加上从库数据源,自动切换,是不是就能实现读写分离?


MyBatisX 插件


MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率

但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表 联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可 以使用MyBatisX插件

MyBatisX一款基于 IDEA 的快速开发插件,为效率而生

image.png

image-20230106212213026


image-20230106212226329


image-20230106212301945

  • Title: MyBatis-plus
  • Author: cccs7
  • Created at : 2023-01-01 12:54:12
  • Updated at : 2024-09-13 11:07:59
  • Link: https://cs7eric.github.io/2023/01/01/MyBatis-plus/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments