Mybatis、Spring、SpringMVC
Mybatis本身就是被用作简化我们CRUD过程的一个框架,而MybatisPlus是和Mybatis配合使用的,可以更加简化我们的CRUD过程,可以自动化完成CRUD。
官网原话:MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。MybatisPlus的愿景是成为 MyBatis 最好的搭档,就像魂斗罗中的 1P、2P一样,基友搭配,效率翻倍。
https://mp.baomidou.com/guide/
由于编写该文章的时候,没有使用最新的MybatisPlus版本,而目前MybatisPlus已经更新了非常多的版本了,其中很多下文设计的操作会有不同,但是总体来说是差不多的,我使用的3.0.5MP展示了更多配置细节,而最新版则省略或者改变了一些配置方式,如果你使用的是最新版的或者和我的版本差距比较大的话只需要去官方文档查看使用方法即可,基本上都是相通的。
学习新组件的通用思路:
创建springboot项目(只需要勾选web即可)
引入依赖
这里需要mysql的依赖和mybatisplus的依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
创建数据库并插入数据,实例数据库创建语句和插入语句如下
CREATE TABLE USER
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
INSERT INTO USER (id, NAME, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
连接数据库
在application.yml或properties文件中配置数据库参数
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/你的数据库名
?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
对比传统mybatis和使用了mybatis-plus后的区别
传统:pojo->dao(使用mybatis进行映射)->service->controller
mybatis-plus:pojo->mapper(继承BaseMapper类)->即可完成大部分简单的增删改查分页等任务
@Mapper
@Repository
//继承BaseMapper类即可
public interface UserMapper extends BaseMapper<User> {
}
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
测试结果为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CCs02Flx-1637981062998)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20211120113947137.png)]
由于我们现在的SQL语句都是自动化的,我们看不见它的运行情况,但我们又时常希望可以看到它是用什么SQL语句来执行的还有很多运行中的其他信息,所以需要日志来帮助我们将对数据库的操作可视化。
在application.yml或properties文件中配置数据库参数
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 在控制台输出,也可以用其他的日志方式
测试结果
我们可以看到,执行的SQL语句为:
使用的连接和连接池为:
User user = new User();
user.setName("dt");
user.setAge(3);
user.setEmail("[email protected]");
userMapper.insert(user);
我们发现,插入的时候,mybatis-plus自动给我们设置了一个ID(主键),且这个ID肉眼看起来没有什么规律。
我们在插入中,由于ID是主键,所以mp给我们自动用分布式全局唯一ID算法得到一个全局唯一ID。我们阅读文档发现,可以在POJO实体类中的主键属性上增加一个注解 @TableId ,该注解可以设置主键的生成方法/算法,具体参数如下
User user = new User();
user.setName("faker");
user.setId(0L);
user.setEmail("[email protected]");
userMapper.updateById(user);
User user = new User();
user.setName("rookie");
user.setId(0L);
userMapper.updateById(user);
控制台,此时的预SQL语句和插入元素为:
我们发现,我们的更新函数并没有发生变化,参数也没有发生变化,都是一个user对象,但是执行的SQL语句却不一样,它会根据我们对user赋值的情况来对SQL语句作出改变,也就是动态SQL,MP直接帮我们做了动态SQL这件事,如果使用的还是mybatis则需要我们自己手动写动态SQL,这也是MP相比传统mybatis更加方便的地方
我们在操作数据库表中的数据的时候,往往需要记录被操作的数据的创建时间和修改时间,方便以后查询某个数据是什么时候创建的和什么时候修改的,像阿里巴巴开发手册所说的,他们会在每个表里面加上gmt_create .gmt_modified两个字段来作为创建时间和修改时间。
而这种仅作为记录时间的字段,往往不应该每次操作的时候都调用编写代码来实现,这样过于低效,由于是固定化的任务,我们完全可以让它自动化实现,这就是为什么我们要自动填充字段。
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
User user = new User();
user.setName("meiko");
user.setAge(10);
user.setEmail("[email protected]");
userMapper.insert(user);
也可以直接在数据库创建的时候修改字段为自动填充,或者在需要的时候,往表里添加字段的同时设置为自动填充。
但我不建议用这样的方式来做自动填充,因为真实开发环境中,往往数据库的权限不在开发方的手上,难以对数据库进行直接的修改,而且在数据库表使用后再增加字段是很大的禁忌,一般来说都是不会发生的。我们使用代码来解决问题,更加的灵活,也更加的普适,如果想要了解这种方法的话可以自动查找资料,作为了解即可。
乐观锁:顾名思义十分乐观,他总是认为不会出现问题,无论干什么都不去上锁,如果出现了问题,就会再次更新值进行测试
有乐观锁也就有悲观锁,悲观锁和乐观锁相对
悲观锁:顾名思义十分悲观,他总是认为出现问题,无论干什么都会先上锁再操作
首先需要添加乐观锁的数据库表中添加version字段,并在MP中手动乐观锁添加组件
然后:
当需要更新数据的时候,在where语句后匹配version字段是否正确,如果正确则对数据进行修改,并让version自增,否则操作失败,原SQL语句不执行(执行失败)
1.增加version字段
@Version
private Integer version;
2.注册乐观锁
@MapperScan("com.xiafan.mapper")
@EnableTransactionManagement
@Configuration // 配置类
public class MybatisPlusConfig {
// 注册乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
}
3.测试乐观锁,测试串行更新和模拟多线程更新测试
//串行,可以正常运行
@Test
void testVersion1(){
User user = userMapper.selectById(1461964912795344897L);
user.setName("ququ");
user.setAge(18);
userMapper.updateById(user);;
}
//模拟多线程,user和user1对同一行数据进行更新,但在user还没更新的时候user1已经获取了原来user获取的对象了,此时两个对象version都是2,但是当user1提交后,version自增为3,再提交user时,由于此时需要验证version为2,但version已经自增为3了,所以该更新语句失效了
@Test
void testVersion2(){
User user = userMapper.selectById(1461964912795344897L);
user.setName("qq");
user.setAge(15);
User user1 = userMapper.selectById(1461964912795344897L);
user1.setName("wx");
user1.setAge(16);
userMapper.updateById(user1);
userMapper.updateById(user);
}
所调用的方法如下:
两种按ID查询的方法不必多说,重点演示一下如何进行条件查询,演示代码如下:
Map<String, Object> map = new HashMap<>();
//查询 id=1 and age=20 的数据
map.put("id",1L);
map.put("age",20);
userMapper.selectByMap(map);
//注册分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
@Test
void testPage(){
//第一个参数为当前页面,第二个参数为每一页的数据个数(行数)
Page<User> objectPage = new Page<>(1,5);
userMapper.selectPage(objectPage,null);
objectPage.getRecords().forEach(System.out::println);
}
所调用的方法如下:
两种按ID删除的方法不必多说,重点演示一下如何进行条件删除,演示代码如下:
Map<String, Object> map = new HashMap<>();
//查询 id=1 and age=20 的数据
map.put("id",0L);
map.put("name","xiaohu");
userMapper.deleteByMap(map);
逻辑删除之所以叫逻辑删除,是因为它和物理删除这种真实删除不一样,区别如下:
物理删除:从存储介质中完全抹去了这个数据,这个数据永远的删除了,无法再得到它
逻辑删除:我们通过某些方法,比如增加一个字段,当字段=0的时候,默认它是存在的,当字段=1的时候,默认它被删除了,但是它其实并没有被删除,只是在做其他操作的时候会被忽略掉,只是在“逻辑上”被删除了,所以被叫做逻辑删除
逻辑删除的实现和乐观锁的实现非常相似。
@TableLogic
private Integer deleted;
//注册逻辑删除插件
@Bean
public ISqlInjector sqlInjector(){
return new LogicSqlInjector();
}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 在控制台输出,也可以用其他的日志方式
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0
本质上,逻辑删除就是把”删除“变为了”更新“,我们只要增加一个标记字段,然后约定好这个字段为某个值代表它应该被”看见“,为某个别的值代表它不能被”看见“,就可以实现看起来好像删除了,但其实数据还存在我们的存储介质中的效果
条件构造器是用来让MP能够进行复杂SQL操作的工具,如模糊查询,分组查询等,这些复杂操作只使用MP的BaseMapper里已经封装好的方法是不够用的(不加Wrapper类参数的话)
条件构造器的使用是建立在Wrapper类上的,Wrapper类封装了绝大部分SQL语句的操作,且支持链式编程(即可以连续设置参数)
实例:这里只实例一部分方法,其他的去看官方文档即可,用法简单易懂
//>=
@Test
void test1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.ge("age","28")
.isNotNull("name")
.select("name");
userMapper.selectList(wrapper).forEach(System.out::println);
}
//between
@Test
void test2(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.between("age",20,30);
List<User> userList = userMapper.selectList(wrapper);
for (User user : userList){
System.out.println(user.getName());
}
}
//模糊查询like
@Test
void test3(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.likeRight("name","J")
.notLike("name","a");
List<User> userList = userMapper.selectList(wrapper);
for (User user : userList){
System.out.println(user.getName());
}
}
本质是使用MP封装好的类对所需生成的代码文件进行配置,这一块没有什么技术上的要求,这里使用官方文档的例子作为说明:
// 演示例子,执行 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("jobob");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/ant?useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("密码");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("com.baomidou.ant");
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.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// 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.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}