官方文档帮助文档: 简介 | MyBatis-Plus (baomidou.com)
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
MyBatis-Plus 提供了 通用 mapper 接口 和 service ,不需要写sql 语句,直接生成 sql 语句。
任何能使用
MyBatis
进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。
a、引入mybatis-plus依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
b、使用 SpringBoot自带的数据源 com.zaxxer.hikari.HikariDataSource 也行,使用Druid数据源也可以
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
c、配置数据库信息
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
password: root
username: root
type: com.alibaba.druid.pool.DruidDataSource
d、创建实体类
@Data
//MyBatis-Plus自动根据实体类小写去对应数据库中的表。可以通过 @TableName 指定表名
@TableName("t_user")
public class User {
private Long id ;
private String userName ;
private String password ;
private String realName ;
}
e、mapper接口
public interface UserMapper extends BaseMapper<User> {
//不需要编写 sql 方法,直接继承 BaseMapper 。
}
f、主程序中【配置类上】,配置 Mapper接口扫描【或者在 mapper 接口上加上 @Mapper 注解】
//扫描mapper包下的所有mapper接口
@MapperScan("com.example.mapper")
g、测试
class MybatisPlusApplicationTests {
@Autowired
//userMapper 会报错
//运行时是没问题,因为 在 SpringBoot 中默认接口是无法实例化的。但实际上 mapper 交给了 动态代理类了。
private UserMapper userMapper ;
@Test
@DisplayName("查询")
void contextLoads() {
//queryWrapper: 条件构造器,如果没有条件就 null
List<User> users = userMapper.selectList(null);
for (User user : users) {
System.out.println(user);
}
}
}
h、增加日志功能
mybatis-plus:
configuration:
map-underscore-to-camel-case: false #不使用驼峰命名转换。默认是开启的。
# 开启日志功能
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意:
mybatis 会自动将实体类中的属性名转换为符合数据库的字段名
比如:属性名= userName ====> 自动转换为:user_name
如果你数据库该字段名为 userName 是会报错的。
Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column ‘user_name’ in ‘field list’
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);
@Test
@DisplayName("增加")
void test_insert() {
//INSERT INTO t_user ( id, userName, password, realName ) VALUES ( ?, ?, ?, ? )
int res = userMapper.insert(new User(null, "markbolo", "0000", "马克波罗"));
//断言
//id = 1504988968188436481 是通过雪花算法生成的iD
Assertions.assertEquals(1,res);
}
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 map 集合 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/**
* 删除(根据ID或实体 批量删除)
*
* @param idList 主键ID列表或实体列表(不能为 null 以及 empty)
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<?> idList);
@Test
@DisplayName("根据ID删除")
void test_deleteById() {
// DELETE FROM t_user WHERE id=?
int res = userMapper.deleteById(1504988968188436481L); //超出 int 类型范围,加个 L 表示long类型
//断言
Assertions.assertEquals(1,res);
}
@Test
@DisplayName("根据条件删除")
void test_deleteByMap() {
// DELETE FROM t_user WHERE realName = ? AND password = ?
Map<String,Object> map = new HashMap<>();
//将查询条件封装到map集合中
map.put("realName","老五");
map.put("password","0000");
int res = userMapper.deleteByMap(map);
//断言
Assertions.assertEquals(1,res);
}
@Test
@DisplayName("根据ID批量删除")
void test_deleteBatch() {
// DELETE FROM t_user WHERE realName = ? AND password = ?
List<Long> list = Arrays.asList(1504988968188436482L, 1504988968188436483L, 1504988968188436484L);//转换成一个list集合
int res = userMapper.deleteBatchIds(list);
//断言
Assertions.assertEquals(3,res);
}
ID 是通过雪花算法生成的 long 类型。
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);
@Test
@DisplayName("根据ID修改")
void test_updateByID() {
//UPDATE t_user SET userName=?, password=?, realName=? WHERE id=?
//根据实体类ID 进行修改
int res = userMapper.updateById(new User(1504988968188436485L,"rose","0000","rose"));
//断言
Assertions.assertEquals(1,res);
}
@Test
@DisplayName("根据ID查询单个用户信息")
void test_selectForOne() {
//SELECT id,userName,password,realName FROM t_user WHERE id=?
User user= userMapper.selectById(23L);
System.out.println(user);
}
@Test
@DisplayName("根据ID查询多个用户信息")
void test_selectForMore() {
//SELECT id,userName,password,realName FROM t_user WHERE id IN ( ? , ? , ? )
List<Long> ids = Arrays.asList(23L, 35L, 30L);
List<User> users = userMapper.selectBatchIds(ids);
System.out.println(users);
}
@Test
@DisplayName("根据map和查询用户信息")
void test_selectForMap() {
//SELECT id,userName,password,realName FROM t_user WHERE id = ? AND userName = ?
Map<String,Object> map = new HashMap<>();
//将查询条件封装在 map集合中
map.put("userName","rose");
map.put("id",1504988968188436485L);
List<User> users = userMapper.selectByMap(map);
System.out.println(users);
}
@Test
@DisplayName("查询所有用户信息")
void test_selectForList() {
//SELECT id,userName,password,realName FROM t_user
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
如果 baseMapper 提供的方法不满足需求,可以自定义sql。
和 mybatis 一样。在 mapper 接口写 sql 方法。mapper 映射文件里 sql 语句。
和 mybatis 不一样的是,在SpringBoot 中 mapper映射文件有默认位置: classpath*:/mapper/**/*.xml
也可以通过:mybatis-plus-mapper-locations:
指定mapper映射文件位置
说明:
get 查询单行
remove 删除
list 查询集合
save 插入
page 分页
前缀命名方式区分 Mapper
层避免混淆。在 mapper 中: select 查询,delete 删除,insert 增加 ,update 修改
T
为任意实体对象service
接口 和实现类 ,继承 MyBatis-Plus 提供的 IService
和 ServiceImpl
Wrapper
为 条件构造器MyBatis-Plus 提供了 service 接口 和 实现类,但是为了满足开发需要,
public interface UserService extends IService<User> {
// IService :泛型为 实体类类型。
}
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// ServiceImpl : MyBatis-Plus 提供的 IService 的实现类。
//第一个泛型:mapper 接口
//第二个泛型“:实体类对象
}
IService 提供的 批量增加功能:
@Autowired
UserService userService ;
@Test
@DisplayName("批量增加")
void test_saveBatch() {
List<User> users = Arrays.asList(
new User(null, "aaa", "000", "aaa"),
new User(null, "bbb", "000", "bbb"),
new User(null, "ccc", "000", "ccc"));
//参数是一个list集合
boolean saveBatch = userService.saveBatch(users);
System.out.println(saveBatch);
}
1、@TableName : MyBatis-Plus 默认根据实体类名找数据库中的表,如果不一致,可通过 此注解指定表名。
@TableName("t_user")
: 表示对应数据库表名为:t_user
还可以通过 全局配置 指定表名前缀:
# 设置数据库表名的前缀
mybatis-plus:
global-config:
db-config:
table-prefix: t_user
2、@TableId : 标注在属性上,将该属性对应的 字段名 看做主键。
value 属性: @TableId(value = " ") 指定 主键 id 名
type 属性 :@TableId(type = IdType.AUTO) 设置主键生成策略为自增。【默认是使用雪花算法 ASSIGN_ID】
使用条件:数据库中的主键也必须设置为 自增。
可以通过全局配置设置所有主键为自增:
mybatis-plus:
global-config:
db-config:
# 设置全局 id 自增功能
id-type: auto
3、**@TableField ** : 设置属性名对应的字段名。
@TableField(value = "user_name") 当属性名和字段名不一致时,可通过 @TableField 注解指定字段名。
4、@TableLogic :
在数据库中增加 is_delete
字段,用来表示删除状态。 1 表示逻辑删除 0 表示未删除
//逻辑删除语句,其实是修改语句,将删除的数据的删除状态 更改为 1
UPDATE t_user SET is_delete=1 WHERE uid IN ( ? , ? , ? ) AND is_delete=0
数据库中仍然有删除后的数据,但是使用查询语句 是查询不出来的。
在使用条件构造器时,默认用 and 拼接
@Test
@DisplayName("条件构造器")
public void test01(){
//比如:需要查询 用户名带 a 并且 密码不为空 的用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("user_name","a").isNotNull("password");
List<User> list = userService.list(queryWrapper);
list.forEach(System.out::println);
}
条件构造器支持链式增加,并且方法名的意思和数据库中的 关键字 的意思一样。
参考: 条件构造器 | MyBatis-Plus (baomidou.com)
@Test
@DisplayName("组装排序条件")
public void test02(){
//比如:按照id的降序查询
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("uid");
// queryWrapper.orderByAsc("uid"); 升序
List<User> list = userService.list(queryWrapper);
list.forEach(System.out::println);
}
@Test
@DisplayName("组装删除条件")
public void test03(){
//比如:删除密码为空的数据
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("password");
boolean remove = userService.remove(queryWrapper);
System.out.println(remove);
}
@Test
@DisplayName("组装修改条件--queryWrapper")
public void test04(){
//比如:修改 用户名中带有 a 或者 密码为 0000 的真实姓名为 小明
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("user_name","a")
//默认组装是由 and 拼接的。 使用 or() 用 or 拼接
.or()
.eq("password","0000");
//update() 中有俩个参数:
//1、实体类 用来修改信息
//2、queryWrapper:用来获取可以修改的用户信息。
User user = new User();
user.setUserName("小明");
boolean update = userService.update(user, queryWrapper);
System.out.println(update);
}
使用 and(Consumer consumer)
方法,会将 ( ) 里面的条件用 括号 括起来。提高优先级。
里面的 参数就是一个 wrapper
。
以下代码执行的 sql 语句:
UPDATE t_user SET real_name=? WHERE is_delete=0 AND (user_name LIKE ? AND (password = ? OR real_name IS NULL))
@Test
@DisplayName("条件优先级")
public void test05(){
//比如:修改 1、用户名中带有 a 并且 2、(密码为 0000 或者 真实姓名为 null ) 的真实姓名为 小红
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("user_name","a")
//MyBaits中的 lambda表达式是优先执行的。
.and(i -> i.eq("password","0000").or().isNull("real_name"));
User user = new User();
user.setRealName("小红");
boolean update = userService.update(user, queryWrapper);
System.out.println(update);
}
目前位置默认查询出来的字段是所有字段,如何查询出来为指定的字段
QueryWrapper
@Test
@DisplayName("组装 select 子句")
public void test06(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//指定查询字段
queryWrapper.select("user_name","real_name");
List<Map<String, Object>> maps = userService.listMaps(queryWrapper);
maps.forEach(System.out::println);
}
前面在进行修改时: 使用 queryWrapper
用来设置修改条件,实体类
用来设置修改内容
updateWrapper : 既可以 用来设置 修改条件,又可以设置修改的内容。
@Test
@DisplayName("组装修改条件 --- updateWrapper ")
public void test08(){
比如:修改 用户名中带有 a 或者 密码为 0000 的真实姓名为 小明
UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>();
//修改条件
userUpdateWrapper.like("user_name","a").or().eq("password","0000");
//进行修改
userUpdateWrapper.set("real_name","小明");
boolean update = userService.update(userUpdateWrapper);
System.out.println(update);
}
下面模拟以下 在开发中的场景:
@Test
@DisplayName("condition")
public void test09(){
//假设以下条件参数是从浏览器传送过来的。
String userName = "小明" ;
String password = "0000" ;
String realName = "";
//进行查询。但是不需要将 空的参数作为查询条件,就需要增加判断
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(userName)){
//如果userName不为空,就作为查询条件
queryWrapper.like("user_name",userName) ;
}
if (StringUtils.isNotBlank(password)){
//如果 password 不为空,就作为查询条件
queryWrapper.like("password",password) ;
}
if (StringUtils.isNotBlank(realName)){
//如果 realName 不为空,就作为查询条件
queryWrapper.like("real_name",realName) ;
}
List<User> list = userService.list(queryWrapper);
list.forEach(System.out::println);
}
这样判断是非常麻烦的,可以通过 condition
这个参数用来判断。
每一个 方法里都会提供一个 condition
参数,这个参数就是用来 判断该条件是否组装在sql语句后面
@Test
@DisplayName("condition")
public void test010(){
//假设以下参数是从浏览器传送过来的。
String userName = "小明" ;
String password = "0000" ;
String realName = "";
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//第一个参数:condition : 组装条件
//第二个参数:字段名
//第三个参数:条件
queryWrapper.like(StringUtils.isNotBlank(userName),"user_name",userName)
.like(StringUtils.isNotBlank(password),"password",password)
.like(StringUtils.isNotBlank(realName),"real_name",realName) ;
List<User> list = userService.list(queryWrapper);
list.forEach(System.out::println);
}
开启分页功能:
//开启分页功能
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor (){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor() ;
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor ;
}
分页使用:
@Test
public void test01(){
// limit start,size start :表示起始索引,size:显示条数
//current : 当前页码 。 (当前页码 -1) * size = start
// size :每页显示条数
Page<User> page = new Page<>(1,3);
//查询并且分页
userService.page(page);
System.out.println(page);
}
page 中的属性:
records : 每页的数据
pages : 总页数
total : 总记录数
current : 当前页码
size :每页显示条数
hasNext :是否有下一页
hasPrevious : 是否有上一页
有这样一个场景:
一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把
商品价格增加50元
。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太
高,可能会影响销量。又通知小王,你把
商品价格降低30元
。此时,
小李和小王同时操作商品后台系统
。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据
库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就
完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1
万多
也就是说当俩个人同时操作数据库时,都是对同一条数据同时进行的修改。和 银行取钱的场景类似
上面的故事
如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证
最终的价格是120元。
模拟修改冲突:
@Test
public void test11(){
//小李查询价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询的价格 :" + productLi.getPrice());
//小王查询价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询的价格 :" + productWang.getPrice());
//小王和小李获取的价格都是 100
//小李修改价格 150
productLi.setPrice(productLi.getPrice()+50);
productMapper.updateById(productLi);
System.out.println("小李修改完价格:" + productLi.getPrice());
//小王修改价格 70 小王修改的价格会覆盖小李修改的价格
productWang.setPrice(productWang.getPrice()-30);
productMapper.updateById(productWang);
System.out.println("小王修改完价格:" + productWang.getPrice());
//老板查询价格 70
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询的价格:" + productBoss.getPrice());
//
}
在老板查询的时候,商品价格为 70 ,小王的修改已经覆盖了小李的修改。
乐观锁实现流程 :
实现乐观锁一般是在数据库中 增加
version
字段,表示版本号,在修改数据前获取版本号。修改数据时不仅要修改数据,还要修改版本号。【version+1】比如:小王和小李第一次获取价格和版本号时,
price=100, version=1
小李在修改数据时,先获取版本号=1,和第一次获取价格时的版本号一致,就可以修改:
price 100 + 50 = 150
,version: 1+1=2
小王在修改数据时,先获取
version = 2
【因为小李在修改时将版本号增加了 1 】,由于版本号和之前获取的不一致,所以小王的修改操作就不会成功。最后price=150
增加乐观锁插件 :
a、在属性上增加 @Version
注解
@Version //表示乐观锁版本号字段
private Integer version ;
b、增加乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor (){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor() ;
//增加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//增加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor ;
}
最后 老板查询的价格:150.0
因为在小王想要修改价格的时候,获取到的版本号和第一次获取的时候不一致,所以并没有修改成功。
优化修改流程,使小王的操作成功:
在小王进行修改时,修改失败后在重新查询,修改价格。
@Test
public void test11(){
//小李查询价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询的价格 :" + productLi.getPrice());
//小王查询价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询的价格 :" + productWang.getPrice());
//小王和小李获取的价格都是 100
//小李修改价格 150
productLi.setPrice(productLi.getPrice()+50);
productMapper.updateById(productLi);
System.out.println("小李修改完价格:" + productLi.getPrice());
//小王修改价格 70 小王修改的价格会覆盖小李修改的价格
productWang.setPrice(productWang.getPrice()-30);
int res = productMapper.updateById(productWang);
if (res ==0 ){
//结果=0,说明小王操作失败。就重新查新,重新修改
Product newProduct = productMapper.selectById(1);
newProduct.setPrice(newProduct.getPrice() - 30);
productMapper.updateById(newProduct) ;
}
System.out.println("小王修改完价格:" + productWang.getPrice());
//老板查询价格
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询的价格:" + productBoss.getPrice());
}
在表中有些字段值是固定的,比如性别:只有 男和女,这时候就可以通过枚举来实现。
@EnumValue + type-enums-package
实现该功能
增加枚举类:
@Getter
public enum SexEnum {
MALE(1, "男"),
FEMALE(2, "女");
//将注解标识的属性值存储到数据库。
@EnumValue
private Integer sex;
private String sexName;
SexEnum(Integer sex, String sexName) {
this.sex = sex;
this.sexName = sexName;
}
}
配置全局文件扫描枚举:
mybatis-plus:
type-enums-package: com.example.enums
测试:
@Test
public void test01(){
User user = new User();
user.setUserName("lisi");
user.setRealName("李四");
user.setPassword("0000");
//使用枚举赋值
user.setSex(SexEnum.MALE);
int insert = userMapper.insert(user);
System.out.println(insert);
}
a、引入依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>2.3.30version>
dependency>
b、自动生成代码,可直接拷贝
FastAutoGenerator.create(
"jdbc:mysql://localhost:3306/book",
"root",
"root")
.globalConfig(builder -> {
builder.author("yang") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("C://user//"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.example") // 设置父包名
.moduleName("") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "C://user//")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_book") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等
a、引入依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>3.5.0version>
dependency>
b、增加配置
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/test
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://localhost:3306/book
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
c、只需要在 service 使用 @DS()
注解指定数据源即可。
@DS
可以标注在方法上或者类上【就近原则,方法优先】
@Service
@DS("master") //指定master数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
@Service
@DS("slave_1") //指定数据源
public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements IBookService {
}
插件搜索:MyBatisX 插件 下载安装 重启