Mybatis-Plus 常用操作
MyBatis-Plus系列推荐阅读顺序:
本文目录结构
一、SQL日志开关
二、常用注解
三、代码生成器
四、分页查询
五、Mybatis-Plus Wrapper
六、自动填充数据功能
七、逻辑删除
八、乐观锁
一、SQL日志开关
配置文件application.properties
,增加最后一行,执行时会打印出 sql 语句。
spring.application.name=mybatis-plus
# 应用服务 WEB 访问端口
server.port=8080
####数据库连接池###
spring.datasource.url=jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=guo
spring.datasource.password=205010guo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
####输出sql日志###
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
类似JPA的日志输出配置:
jpa:
show-sql:true#打印SQL。
二、常用注解
注解说明的官方文档:https://mybatis.plus/guide/annotation.html
2.1【@TableName 】
@TableName 用于定义表名
注:
常用属性:
value 用于定义表名
2.2【@TableId】
@TableId 用于定义表的主键
注:
常用属性:
value 用于定义主键字段名
type 用于定义主键类型(主键策略 IdType)
主键策略:
IdType.AUTO 主键自增,系统分配,不需要手动输入
IdType.NONE 未设置主键
IdType.INPUT 需要自己输入 主键值。
IdType.ASSIGN_ID 系统分配 ID,用于数值型数据(Long,对应 mysql 中 BIGINT 类型)。
IdType.ASSIGN_UUID 系统分配 UUID,用于字符串型数据(String,对应 mysql 中 varchar(32) 类型)。
2.3【@TableField】
@TableField 用于定义表的非主键字段。
注:
常用属性:
value 用于定义非主键字段名
exist 用于指明是否为数据表的字段, true 表示是,false 为不是。
fill 用于指定字段填充策略(FieldFill)。
字段填充策略:(一般用于填充 创建时间、修改时间等字段)
FieldFill.DEFAULT 默认不填充
FieldFill.INSERT 插入时填充
FieldFill.UPDATE 更新时填充
FieldFill.INSERT_UPDATE 插入、更新时填充。
2.4【@TableLogic】
@TableLogic 用于定义表的字段进行逻辑删除(非物理删除)
注:
常用属性:
value 用于定义未删除时字段的值
delval 用于定义删除时字段的值
2.5【@Version】
@Version 用于字段实现乐观锁
三、代码生成器
3.1 AutoGenerator 简介
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。 与 mybatis 中的 mybatis-generator-core 类似。
3.2 添加依赖
<!-- 代码生成器 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mp.version}</version>
</dependency>
<!-- 添加 模板引擎 依赖 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
3.3 生成器代码分析
Step1:
创建一个 代码生成器。用于生成代码。
此处不用修改。
// Step1:代码生成器
AutoGenerator mpg = new AutoGenerator();
Step2:
配置全局信息。指定代码输出路径,以及包名、作者等信息。
此处按需添加,projectPath 需要修改,setAuthor 需要修改。
// Step2:全局配置
GlobalConfig gc = new GlobalConfig();
// 填写代码生成的目录(需要修改)
String projectPath = "E:\\myProject\\test\\test_mybatis_plus";
// 拼接出代码最终输出的目录
gc.setOutputDir(projectPath + "/src/main/java");
// 配置开发者信息(可选)(需要修改)
gc.setAuthor("郭秀志 jbcode@126.com");
// 配置是否打开目录,false 为不打开(可选)
gc.setOpen(false);
// 实体属性 Swagger2 注解,添加 Swagger 依赖,开启 Swagger2 模式(可选)
//gc.setSwagger2(true);
// 重新生成文件时是否覆盖,false 表示不覆盖(可选)
gc.setFileOverride(false);
// 配置主键生成策略,此处为 ASSIGN_ID(可选)
gc.setIdType(IdType.ASSIGN_ID);
// 配置日期类型,此处为 ONLY_DATE(可选)
gc.setDateType(DateType.ONLY_DATE);
// 默认生成的 service 会有 I 前缀
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
Step3:
配置数据源信息。用于指定 需要生成代码的 数据仓库、数据表。
setUrl、setDriverName、setUsername、setPassword
均需修改。
// Step3:数据源配置(需要修改)
DataSourceConfig dsc = new DataSourceConfig();
// 配置数据库 url 地址
dsc.setUrl("jdbc:mysql://localhost:3306/testMyBatisPlus?useUnicode=true&characterEncoding=utf8");
// dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定数据库名
// 配置数据库驱动
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
// 配置数据库连接用户名
dsc.setUsername("root");
// 配置数据库连接密码
dsc.setPassword("123456");
mpg.setDataSource(dsc);
Step4:
配置包信息。
setParent、setModuleName
均需修改。其余按需求修改.
// Step:4:包配置
PackageConfig pc = new PackageConfig();
// 配置父包名(需要修改)
pc.setParent("com.erbadagang.mybatis.plus");
// 配置模块名(需要修改)
//pc.setModuleName("mybatis-plus-starter");
// 配置 entity 包名
pc.setEntity("entity");
// 配置 mapper 包名
pc.setMapper("mapper");
// 配置 service 包名
pc.setService("service");
// 配置 controller 包名
pc.setController("controller");
mpg.setPackageInfo(pc);
Step5:
配置数据表映射信息。
setInclude 需要修改,其余按实际开发修改。
// Step5:策略配置(数据库表配置)
StrategyConfig strategy = new StrategyConfig();
// 指定表名(可以同时操作多个表,使用 , 隔开)(需要修改)
strategy.setInclude("t_user");
// 配置数据表与实体类名之间映射的策略
strategy.setNaming(NamingStrategy.underline_to_camel);
// 配置数据表的字段与实体类的属性名之间映射的策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 配置 lombok 模式
strategy.setEntityLombokModel(true);
// 配置 rest 风格的控制器(@RestController)
strategy.setRestControllerStyle(true);
// 配置驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
// 配置表前缀,生成实体时去除表前缀
// 此处的表名为 test_mybatis_plus_user,模块名为 test_mybatis_plus,去除前缀后剩下为 user。
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
表t_user
建表SQL:
/*
Navicat Premium Data Transfer
Source Server : 上海
Source Server Type : MySQL
Source Server Version : 50636
Source Host : 101.133.227.13:3306
Source Schema : orders_1
Target Server Type : MySQL
Target Server Version : 50636
File Encoding : 65001
Date: 10/07/2020 16:28:23
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_name` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`password` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`pwd_cipher` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
Step6:
执行代码生成操作。
此处不用修改。
// Step6:执行代码生成操作
mpg.execute();
完整代码:
package com.erbadagang.mybatis.plus.mybatisplus;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* AutoGenerationTest作用是:生成Mybatis-plus代码,AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
*
* @ClassName: AutoGenerationTest
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/10 15:08
* @Copyright:
*/
@SpringBootTest
public class AutoGenerationTest {
@Test
public void autoGenerate() {
// Step1:代码生成器
AutoGenerator mpg = new AutoGenerator();
// Step2:全局配置
GlobalConfig gc = new GlobalConfig();
// 填写代码生成的目录(需要修改)
String projectPath = "D:\\dev\\GitRepository\\mybatis-plus-starter";
// 拼接出代码最终输出的目录
gc.setOutputDir(projectPath + "/src/main/java");
// 配置开发者信息(可选)(需要修改)
gc.setAuthor("郭秀志 jbcode@126.com");
// 配置是否打开目录,false 为不打开(可选)
gc.setOpen(false);
// 实体属性 Swagger2 注解,添加 Swagger 依赖,开启 Swagger2 模式(可选)
//gc.setSwagger2(true);
// 重新生成文件时是否覆盖,false 表示不覆盖(可选)
gc.setFileOverride(false);
// 配置主键生成策略,此处为 ASSIGN_ID(可选)
gc.setIdType(IdType.AUTO);
// 配置日期类型,此处为 ONLY_DATE(可选)
gc.setDateType(DateType.ONLY_DATE);
// 默认生成的 service 会有 I 前缀
gc.setServiceName("I%sService");
mpg.setGlobalConfig(gc);
// Step3:数据源配置(需要修改)
DataSourceConfig dsc = new DataSourceConfig();
// 配置数据库 url 地址
dsc.setUrl("jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8");
// dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定数据库名
// 配置数据库驱动
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
// 配置数据库连接用户名
dsc.setUsername("guo");
// 配置数据库连接密码
dsc.setPassword("205010guo");
mpg.setDataSource(dsc);
// Step:4:包配置
PackageConfig pc = new PackageConfig();
// 配置父包名(需要修改)
pc.setParent("com.erbadagang.mybatis.plus.mybatisplus");
// 配置模块名(需要修改)
//pc.setModuleName("mybatis-plus-starter");
// 配置 entity 包名
pc.setEntity("entity");
// 配置 mapper 包名
pc.setMapper("mapper");
// 配置 service 包名
pc.setService("service");
// 配置 controller 包名
pc.setController("controller");
mpg.setPackageInfo(pc);
// Step5:策略配置(数据库表配置)
StrategyConfig strategy = new StrategyConfig();
// 指定表名(可以同时操作多个表,使用 , 隔开)(需要修改)
strategy.setInclude("t_user");//表名t_user
// 配置数据表与实体类名之间映射的策略
strategy.setNaming(NamingStrategy.underline_to_camel);
// 配置数据表的字段与实体类的属性名之间映射的策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 配置 lombok 模式
strategy.setEntityLombokModel(true);
// 配置 rest 风格的控制器(@RestController)
strategy.setRestControllerStyle(true);
// 配置驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
// 配置表前缀,生成实体时去除表前缀
// 此处的表名为 test_mybatis_plus_user,模块名为 test_mybatis_plus,去除前缀后剩下为 user。
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
// Step6:执行代码生成操作
mpg.execute();
}
}
3.4 测试生成的service
由于生成的Service接口及实现类有些问题,需要稍为改造一下:
- Service接口:public interface ITUserService extends IService<TUser> 增加泛型:public interface ITUserService
<TUser>
extends IService<TUser> 。 - 实现类:public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements IService<TUser> 实现接口由
IService
变成ITUserService
。
Junit 测试代码:
@Autowired
private ITUserService<TUser> tUserService;
@Test
public void testService() {
TUser user = new TUser();
user.setUserName("trek");
user.setPassword("888999");
user.setPwdCipher("ewifwiEFafe==");
if (tUserService.save(user)) {
tUserService.list().forEach(System.out::println);
} else {
System.out.println("添加数据失败");
}
}
测试结果:
表插入新数据
控制台输出信息:
==> Preparing: INSERT INTO t_user ( user_name, password, pwd_cipher ) VALUES ( ?, ?, ? )
==> Parameters: trek(String), 888999(String), ewifwiEFafe==(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@47acd13b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@633514467 wrapping com.mysql.cj.jdbc.ConnectionImpl@297c9a9b] will not be managed by Spring
==> Preparing: SELECT id,user_name,password,pwd_cipher FROM t_user
==> Parameters:
<== Columns: id, user_name, password, pwd_cipher
<== Row: 1, guo, bwMhZeGXyD98aToKQdXLcw==, null
<== Row: 2, guo, bwMhZeGXyD98aToKQdXLcw==, null
<== Row: 3, guo, 123456, bwMhZeGXyD98aToKQdXLcw==
<== Row: 4, trek, 888999, ewifwiEFafe==
<== Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed]
TUser(id=1, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=2, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=3, userName=guo, password=123456, pwdCipher=bwMhZeGXyD98aToKQdXLcw==)
TUser(id=4, userName=trek, password=888999, pwdCipher=ewifwiEFafe==)
四、分页查询
4.1 配置拦截器组件
MybatisPlusApplication启动类添加代码:
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
4.2 编写分页代码
直接 new 一个 Page 对象,对象需要传递两个参数(当前页,每页显示的条数)。
调用 mybatis-plus 提供的分页查询方法,其会将 分页查询的数据封装到 Page 对象中。
@Test
public void selectPage() {
// 根据Wrapper 自定义条件查询
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", "18");
queryWrapper.orderByDesc("age");
Page<User> userPage = new Page<User>(2, 2);
// userPage.setCurrent(2L); //当前是第几页 默认为1
// userPage.setSize(2); //每页大小
IPage<User> userIPage = userMapper.selectPage(userPage, queryWrapper);
System.out.println("当前页" + userIPage.getCurrent()); //当前页
System.out.println("总页数" + userIPage.getPages()); //总页数
System.out.println("返回数据" + userIPage.getRecords()); //返回数据
System.out.println("每页大小" + userIPage.getSize()); //每页大小
System.out.println("满足符合条件的条数" + userIPage.getTotal()); //满足符合条件的条数
System.out.println("下一页" + userPage.hasNext()); //下一页
System.out.println("上一页" + userPage.hasPrevious()); //上一页
}
运行结果:
结果说明
控制台System.out.println
代码部分日志输出:
当前页2
总页数2
返回数据[User(id=4, name=Oliver, age=21, email=xds@erbadagang.com), User(id=2, name=xiu, age=20, email=specialized@erbadagang.com)]
每页大小2
满足符合条件的条数4
下一页false
上一页true
五、Mybatis-Plus Wrapper
参考上篇文章:MyBatis-Plus 条件构造器(Wrapper)
5.1 删除
/**
* <p>
* 根据根据 entity 条件,删除记录,QueryWrapper实体对象封装操作类(可以为 null)
* 下方获取到queryWrapper后删除的查询条件为name字段为null的and年龄大于等于12的and email字段不为null的
* 同理写法条件添加的方式就不做过多介绍了。
* </p>
*/
@Test
public void delete() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper
.isNull("name")
.ge("age", 12)
.isNotNull("email");
int delete = userMapper.delete(queryWrapper);
System.out.println("delete return count = " + delete);
}
SQL输出:
==> Preparing: DELETE FROM user WHERE (name IS NULL AND age >= ? AND email IS NOT NULL)
==> Parameters: 12(Integer)
<== Updates: 0
5.2 selectOne
/**
* <p>
* 根据 entity 条件,查询一条记录,
* 这里和上方删除构造条件一样,只是seletOne返回的是一条实体记录,当出现多条时会报错
* </p>
*/
@Test
public void selectOne() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "guo");
User user = userMapper.selectOne(queryWrapper);
System.out.println(user);
}
SQL输出:
==> Preparing: SELECT id,name,age,email FROM user WHERE (name = ?)
==> Parameters: guo(String)
<== Columns: id, name, age, email
<== Row: 1, Guo , 18, trek@erbadagang.com
<== Total: 1
5.3 selectCount
/**
* <p>
* 根据 Wrapper 条件,查询总记录数
* </p>
*
* @param queryWrapper 实体对象
*/
@Test
public void selectCount() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "guo");
Integer count = userMapper.selectCount(queryWrapper);
System.out.println(count);
}
SQL输出:
==> Preparing: SELECT COUNT( 1 ) FROM user WHERE (name = ?)
==> Parameters: guo(String)
<== Columns: COUNT( 1 )
<== Row: 1
<== Total: 1
5.4 selectList
/**
* <p>
* 根据 entity 条件,查询全部记录
* </p>
*
* @param queryWrapper 实体对象封装操作类(可以为 null)为null查询全部
*/
@Test
public void selectListByEntity() {
List<User> list = userMapper.selectList(null);//null为无条件
System.out.println(list);
}
/**
* <p>
* 根据 Wrapper 条件,查询全部记录
* </p>
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
@Test
public void selectListByMapper() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "guo");
List<User> list = userMapper.selectList(queryWrapper);//null为无条件
System.out.println(list);
}
5.5 selectMaps
@Test
public void selectMaps() {
Page<User> page = new Page<User>(1, 5);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(map -> {
System.out.println("name-->" + map.get("name"));
System.out.println("email-->" + map.get("email"));
});
System.out.println(maps);
}
返回类型List<Map<String, Object>>。Map的key为字段名称,value为对应的字段值。
控制台输出:
==> Preparing: SELECT id,name,age,email FROM user
==> Parameters:
<== Columns: id, name, age, email
<== Row: 1, Guo , 18, trek@erbadagang.com
<== Row: 2, xiu, 20, specialized@erbadagang.com
<== Row: 3, zhi, 28, giant@erbadagang.com
<== Row: 4, Oliver, 88, winspace@erbadagang.com
<== Row: 5, Messi, 24, look@erbadagang.com
<== Total: 5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@294aba23]
name-->Guo
email-->trek@erbadagang.com
name-->xiu
email-->specialized@erbadagang.com
name-->zhi
email-->giant@erbadagang.com
name-->Oliver
email-->winspace@erbadagang.com
name-->Messi
email-->look@erbadagang.com
[{name=Guo , id=1, age=18, email=trek@erbadagang.com}, {name=xiu, id=2, age=20, email=specialized@erbadagang.com}, {name=zhi, id=3, age=28, email=giant@erbadagang.com}, {name=Oliver, id=4, age=88, email=winspace@erbadagang.com}, {name=Messi, id=5, age=24, email=look@erbadagang.com}]
六、自动填充数据功能
添加、修改数据时,每次都会使用相同的方式进行填充。比如: 数据的创建时间、修改时间、操作者等。
6.1 数据库准备
Mybatis-plus 支持自动填充这些字段的数据。给之前的数据表新增3个字段:创建时间、修改时间、操作人。
SQL语句:
ALTER TABLE `orders_1`.`user`
ADD COLUMN `create_time` datetime(0) COMMENT '创建时间' AFTER `email`,
ADD COLUMN `update_time` datetime(0) COMMENT '修改时间' AFTER `create_time`,
ADD COLUMN `operator` varchar(20) COMMENT '操作人' AFTER `update_time`;
6.2 重新生成代码
并使用 代码生成器重新生成代码,注意修改生成器配置为可覆盖老代码。
// 重新生成文件时是否覆盖,false 表示不覆盖(可选)
gc.setFileOverride(true);
6.3 修改entity
使用@TableField
注解,标注需要进行填充的字段。
package com.erbadagang.mybatis.plus.mybatisplus.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
*
* </p>
*
* @author 郭秀志 jbcode@126.com
* @since 2020-07-11
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {
private static final long serialVersionUID=1929834928304L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 操作人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String operator;
}
填充策略
FieldFill.INSERT_UPDATE
表示插入和更新都进行自动填充。
6.4 自定义MetaObjectHandler
自定义一个类,实现 MetaObjectHandler 接口,并重写方法。添加 @Component 注解,交给 Spring 去管理。
package com.erbadagang.mybatis.plus.mybatisplus.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @description 自定义的数据填充handler,分别写insert和update的写入策略。
* @ClassName: MyFillDataMetaObjectHandler
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/11 9:39
* @Copyright:
*/
@Component
public class MyFillDataMetaObjectHandler implements MetaObjectHandler {
@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, "operator", String.class, "梅西爱骑车");
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "operator", String.class, "梅西爱骑车");
}
}
6.5 测试
6.5.1 插入测试
/**
* 测试插入的自动填充数据功能。
*/
@Test
public void testAutoFillInsert() {
User user = new User();
user.setId(0l);
user.setName("崔克");
user.setAge(18);
user.setEmail("trek@erbadagang.cn");
int id = userMapper.insert(user);//自动返回插入的id
System.out.println(id);
}
运行测试用例,如果报错:Caused by: java.sql.SQLException: Field 'id' doesn't have a default value
需要把id列勾上自增。
输出的SQL信息:
==> Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 崔克(String), 18(Integer), trek@erbadagang.cn(String), 2020-07-11 09:57:43.386(Timestamp), 2020-07-11 09:57:43.388(Timestamp), 梅西爱骑车(String)
<== Updates: 1
表中数据
6.5.2 更新测试
更新name为英文的trek,age为28。
/**
* 测试更新的自动填充数据功能。
*/
@Test
public void testAutoFillUpdate() {
User user = new User();
user.setId(7l);
user.setName("trek");
user.setAge(28);
user.setEmail("trek@erbadagang.cn");
int id = userMapper.updateById(user);//自动返回插入的id
System.out.println(id);
}
运行测试。
输出的SQL信息,只更新了update_time
没更新create_time字段:
==> Preparing: UPDATE user SET name=?, age=?, email=?, update_time=?, operator=? WHERE id=?
==> Parameters: trek(String), 28(Integer), trek@erbadagang.cn(String), 2020-07-11 10:05:30.249(Timestamp), 梅西爱骑车(String), 7(Long)
<== Updates: 1
如果入库的时间跟上面打印的SQL不一致,需要在jdbc连接加入时区设置:
jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
七、逻辑删除
删除数据,可以通过物理删除,也可以通过逻辑删除。
- 物理删除指的是直接将数据从数据库中删除,不保留。
- 逻辑删除指的是修改数据的某个字段,使其表示为已删除状态,而非删除数据,保留该数据在数据库中,但是查询时不显示该数据(查询时过滤掉该数据)。
7.1 表结构
给数据表增加一个字段:delete_flag,用于表示该数据是否被逻辑删除。
SQL语句:
ALTER TABLE `orders_1`.`user`
ADD COLUMN `delete_flag` tinyint(1) COMMENT '逻辑删除(0 未删除、1 删除)' AFTER `operator`;
7.3 使用逻辑删除。
可以定义一个自动填充规则,初始值为 0。0 表示未删除, 1 表示删除。
在Entity类新增:
/**
* 逻辑删除(0 未删除、1 删除)
*/
@TableLogic(value = "0", delval = "1")//定义逻辑删除功能。
@TableField(fill = FieldFill.INSERT)//定义在insert的时候自动填充功能
private Integer deleteFlag;
@TableLogic定义逻辑删除功能,若去除 TableLogic 注解,再执行 Delete 时进行物理删除,直接删除这条数据。
@TableField定义在自动填充功能。
在自动填充规则MyFillDataMetaObjectHandler
类的insertFill
方法添加:
@Override
public void insertFill(MetaObject metaObject) {
......
this.strictInsertFill(metaObject, "deleteFlag", Integer.class, 0);
}
7.4 测试
新增一条闪电牌自行车数据:
User user = new User();
user.setId(0l);
user.setName("闪电");
user.setAge(18);
user.setEmail("specialized@erbadagang.cn");
int id = userMapper.insert(user);//自动返回插入的id
delete_flag字段为自动填充代码定义的默认值0,当然也可以使用数据库定义默认值。
表层面定义的初始值0
新增数据delete_flag值: delete_flag=0
删除数据:
//这次使用IUserService而不是mapper进行测试
@Autowired
private IUserService userService;
/**
* 逻辑删除测试。
*/
@Test
public void testDelete() {
if (userService.removeById(8)) {
System.out.println("删除数据成功");
userService.list().forEach(System.out::println);
} else {
System.out.println("删除数据失败");
}
}
执行测试,输出的日志:
==> Preparing: UPDATE user SET delete_flag=1 WHERE id=? AND delete_flag=0
==> Parameters: 8(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@22bdb1d0]
删除数据成功
==> Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag FROM user WHERE delete_flag=0
==> Parameters:
<== Total: 0
可以看到更新
delete_flag=1
的操作,以及查询时自动加上了WHERE delete_flag=0
的判断。
表数据变化:
delete_flage变为1
八、乐观锁
8.1 基础知识
(1)首先认识一下: 读问题、写问题
操作数据库数据时,遇到的最基本问题就是 读问题与写问题。
读问题 指的是从数据库中读取数据时遇到的问题,比如:脏读、幻读、不可重复读。
脏读、幻读、不可重复读 参考地址
写问题 指的是数据写入数据库时遇到的问题,比如:丢失更新(多个线程同时对某条数据更新,无论执行顺序如何,都会丢失其他线程更新的数据)
(2)如何解决写问题?
乐观锁、悲观锁就是为了解决 写问题而存在的。
乐观锁:总是假设最好的情况,每次读取数据时认为数据不会被修改(即不加锁),当进行更新操作时,会判断这条数据是否被修改,未被修改,则进行更新操作。若被修改,则数据更新失败,可以对数据进行重试(重新尝试修改数据)。
悲观锁:总是假设最坏的情况,每次读取数据时认为数据会被修改(即加锁),当进行更新操作时,直接更新数据,结束操作后释放锁(此处才可以被其他线程读取)。
(3)乐观锁、悲观锁使用场景?
乐观锁一般用于读比较多的场合,尽量减少加锁的开销。
悲观锁一般用于写比较多的场合,尽量减少 类似 乐观锁重试更新引起的性能开销。
(4)乐观锁两种实现方式
方式一:通过版本号机制实现。
在数据表中增加一个 version 字段。
取数据时,获取该字段,更新时以该字段为条件进行处理(即set version = newVersion where version = oldVersion),若 version 相同,则更新成功(给新 version 赋一个值,一般加 1)。若 version 不同,则更新失败,可以重新尝试更新操作。
方式二:通过 CAS 算法实现。
CAS 为 Compare And Swap 的缩写,即比较交换,是一种无锁算法(即在不加锁的情况实现多线程之间的变量同步)。
CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。如果内存地址里面的值 V 和 A 的值是一样的,那么就将内存里面的值更新成B。若 V 与 A 不一致,则不执行任何操作(可以通过自旋操作,不断尝试修改数据直至成功修改)。即 V == A ? V = B : V = V。
CAS 可能导致 ABA 问题(两次读取数据时值相同,但不确定值是否被修改过),比如两个线程操作同一个变量,线程 A、线程B 初始读取数据均为 A,后来 线程B 将数据修改为 B,然后又修改为 A,此时线程 A 再次读取到的数据依旧是 A,虽然值相同但是中间被修改过,这就是 ABA 问题。可以加一个额外的标志位 C,用于表示数据是否被修改。当标志位 C 与预期标志位相同、且 V == A 时,则更新值 B。
(5)mybatis-plus 实现乐观锁(通过 version 机制)
实现思路:
Step1:取出记录时,获取当前version
Step2:更新时,带上这个version
Step3:执行更新时, set version = newVersion where version = oldVersion
Step4:如果version不对,就更新失败
(6)mybatis-plus 代码实现乐观锁
8.2 MP实现乐观锁
配置乐观锁插件。
启动类MybatisPlusApplication
新增如下代码(类似分页插件),将 OptimisticLockerInterceptor
通过@Bean
交给 Spring 管理。
/**
* 乐观锁插件
* @return 乐观锁插件的实例
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
8.3 定义一个数据库字段 version
ALTER TABLE `orders_1`.`user`
ADD COLUMN `version` int COMMENT '版本号(用于乐观锁, 默认为 1)' AFTER `delete_flag`;
8.4 实体类
使用@Version
注解标注对应的实体类。可以通过@TableField
进行数据自动填充。
/**
* 版本号(用于乐观锁, 默认为 1)
*/
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
8.5 自动填充规则
在自动填充规则MyFillDataMetaObjectHandler
类的insertFill
方法添加:
@Override
public void insertFill(MetaObject metaObject) {
......
//乐观锁version初始化值为1
this.strictInsertFill(metaObject, "version", Integer.class, 1);
}
8.6 测试
/**
* 乐观锁测试
*/
@Test
public void testVersion() {
User user = new User();
user.setName("Look");
user.setAge(8);
user.setEmail("look@erbadagang.cn");
userService.save(user);//新增数据
userService.list().forEach(System.out::println);//查询数据
user.setName("梅花");
userService.update(user, null);//修改数据
userService.list().forEach(System.out::println);//查询数据
}
运行结果(语句增加了我的注释):
##插入数据,version=1
==> Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator, delete_flag, version ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: Look(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西爱骑车(String), 0(Integer), 1(Integer)
<== Updates: 1
##查询数据,读取的version为1
==> Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0
==> Parameters:
<== Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<== Row: 9, Look, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西爱骑车, 0, 1
<== Total: 1
##更新数据,条件是version=1如果此时被其他程序更新了,这里条件不满足不会更新数据。
##version的值自动+1,现在是2。
==> Preparing: UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, operator=?, version=? WHERE delete_flag=0 AND (version = ?)
==> Parameters: 梅花(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西爱骑车(String), 2(Integer), 1(Integer)
<== Updates: 1
##再次查询version为2。
==> Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0
==> Parameters:
<== Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<== Row: 9, 梅花, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西爱骑车, 0, 2
<== Total: 1
##查询出来的最新数据,delete_flag=0
User(id=9, name=梅花, age=8, email=look@erbadagang.cn, createTime=Sat Jul 11 12:03:51 CST 2020, updateTime=Sat Jul 11 12:03:51 CST 2020, operator=梅西爱骑车, deleteFlag=0, version=2)
底线
本文源代码使用 Apache License 2.0开源许可协议,可从Gitee代码地址通过git clone
命令下载到本地或者通过浏览器方式查看源代码。