学习新的技术栈时,一定要多敲,多回复,记笔记
1.概述:mybatis 增强工具 原始不变,只做增强
官网官网:MyBatis-Plus 或 Redirect
对数据库的crud操作封装起来,不用我们书写
只需让接口映射继承BaseMapper即可,<>泛型中就是要操作的数据库表对应的实体类如user表
入门
使用时引入mp依赖,定义配置文件即可
1.1关于主键的生成策略
@TableId注解作用:
- 映射表中主键字段与实体类属性的关系(尤其表中主键字段名称与实体类属性名称不一致时);
- 定义主键生成策略;
//指定主键自增的生成策略
type不指定值时,默认生成的主键iD是基于雪花算法生成的。
@TableId(value = "user_id",type = IdType.AUTO)
生成策略 | 应用场景 | 特点 |
IdType.AUTO | 数据库主键自增(确保数据库设置了 主键自增 否则无效) | 1.使用数据库自带的主键自增值;2.数据库自增的主键值会回填到实体类中;3.数据库服务端生成的; |
IdType.ASSIGN_ID | 主键类型为number类型或数字类型String | 1.MP客户端生成的主键值;2.生成的主键值是数字形式的字符串3.主键对应的类型可以是数字类型或者数字类型的字符串4.底层基于雪花算法,让数据库的唯一标识也参与id的生成运算,保证id在分布式环境下,全局唯一(避免id的主键冲突问题); |
IdType.ASSIGN_UUID | 主键类型为 string(包含数字和字母组成) | 1.生成的主键值包含数字和字母组成的字符串;2.注意事项:如果数据库中主键值是number类型的,可不可用 |
注解@TableField作用:
- 指定表中普通字段与实体类属性之间的映射关系;
- 忽略实体类中多余属性与表中字段的映射关系(@TableField(exist = false));
以下情况可以省略:
- 名称一样
- 数据库字段使用_分割,实体类属性名使用驼峰名称(自动开启驼峰映射)
1.2MP实现常规的增删改查
(1)分页查询
需要定义相关配置:类似于拦截器 用于批量注册
interceptor.addInnerInterceptor(paginationInterceptor);//批量注册要用的bean
1.批量注册mp插件的对象
new MybatisPlusInterceptor();
2.构建分页插件
new PaginationInnerInter
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,-1不受限制
paginationInterceptor.setMaxLimit(-1L);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
/**
* 分页查询:
* 1. 当前页码:currentPage
* 2. 每页显示条数:size
*
* 注意:使用mp的分页要设置一个拦截器!!!
*/
@Test
public void testSelectPage() {
int current = 1;//当前页码
int size = 2;//每页显示条数
IPage<User> page = new Page(current,size);
userMapper.selectPage(page,null);
List<User> records = page.getRecords();//当前页的数据
long pages = page.getPages();//总页数 2
long total = page.getTotal();//总记录数 4
System.out.println(records);
System.out.println(pages);
System.out.println(total);
}
(2)QueryWrapper实现基础查询
wrapper对象封装了条件。
/**
* 基础比较查询
* Wrapper接口:
* 1.QueryWrapper
* LambdaQueryWrapper //查询
* 2.UpdateWrapper
* LambdaUpdateWrapper //更新
*/
eq( ) : 等于 =
ne( ) : 不等于 <> 或者 !=
gt( ) : 大于 >
ge( ) : 大于等于 >=
lt( ) : 小于 <
le( ) : 小于等于 <=
between ( ) : BETWEEN 值1 AND 值2
notBetween ( ) : NOT BETWEEN 值1 AND 值2
in( ) : in
notIn( ) :not in
like():like模糊查询
orderby()
sql中反向查询eg:not like != 等等,查询时是不会走索引的;
查询实现
QueryWrapper逻辑查询or
- 通过QueryWrapper多条件查询时,默认使用and关键字拼接SQL;
- 通过QueryWrapper调用or()方法时,底层会使用or关键字拼接方法左右的查询条件;
QueryWrapper模糊查询like
- like("表列名","条件值"); 作用:查询包含关键字的信息,底层会自动添加匹配关键字,比如:%条件值%
- likeLeft("表列名","条件值"); 作用:左侧模糊搜索,也就是查询以指定条件值结尾的数据,比如:%条件值
- likeRight("表列名","条件值");作用:右侧模糊搜索,也就是查询以指定条件值开头的数据,比如:条件值%
QueryWrapper排序查询
- orderByAsc 升序排序,方法内可传入多个字段
- orderByDesc 降序排序,方法内可传入多个字段
QueryWrapper限定字段查询
select方法说明
MP查询时,默认将表中所有字段数据映射查询,但是有时我们仅仅需要查询部分字段信息,这是可以使用select()方法限定返回的字段信息,避免I/O资源的浪费;
QueryWrapper实现分页条件查询
//参数1:分页对象
//参数2:查询条件
mapper.selectPage(page,wrapper);
(3)LambdaQueryWrapper查询
- 使用QueryWrapper查询数据时需要手写对应表的列名信息,及其容易写错,开发体验不好;
- 使用QueryWrapper查询数据时,表的列名硬编码书写,后期一旦表结构更改,则会带来很大的修改工作量,维护性较差;
LambdaQueryWrapper可以解决上述出现问题,开发推荐使用;
如:wrapper.like(User::getPassword, "123456") 条件方法中用实体类::get方法当第一个参数
@Test
public void testWrapper4() throws Exception{
// LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery();
// wrapper.like("user_name", "%伤%")
// .eq("password","123456")
// .ge("age", 28)
// .between("age",29 , 39); // 包含边界值
wrapper.like(User::getUserName, "%伤%")
.eq(User::getPassword, "123456")
.ge(User::getAge, 28)
.between(User::getAge, 29, 39)
.orderByDesc(User::getAge)
.select(User::getId, User::getUserName);
List<User> users = userMapper.selectList(wrapper);
System.out.println(users);
}
条件删除
条件更新
(4)自定义查询接口实现分页查询
目前我们使用MP自带的分页插件可以很友好的实现分页查询操作,但是如果一些查询需要我们自定义SQL,那该如何实现分页查询操作呢?
- 1自定义接口中直接传入Page分页对象即可;
//@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 查询大于指定id的用户信息,并分页查询实现
* @param page
* @param id
* @return
*/
IPage<User> findGtIdByPage(IPage<User> page, @Param("id") Long id);
}
- 2定义xml映射文件
//@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 查询大于指定id的用户信息,并分页查询实现
* @param page
* @param id
* @return
*/
IPage<User> findGtIdByPage(IPage<User> page, @Param("id") Long id);
}
测试2:
/**
* @Description 自定义sql分页查询实现
*/
@Test
public void test19(){
IPage<User> page=new Page<>(2,3);
IPage<User> users = userMapper.findGtIdByPage(page, 3l);
System.out.println(users.getRecords());
System.out.println(user.getPages());
System.out.println(user.getTotal());
}
5.mp实现Service封装
5.1为了快捷的开发,对业务层Service也进行了封装,直接提供了相关的接口和实现类,开发时,可继承它提供的接口和实现类,使得编码更加高效。
步骤
- 定义一个xxxService接口,该接口继承公共接口IService
- 定义一个xxxServiceImpl服务实现类,该类继承ServiceImpl<Mapper,Entity>,并实现自定义的扩展接口。
注意:1.ServiceImpl父类已经注入了UserMapper对象,名称叫做baseMapper,所以当前实现类直接可以使用baseMapper完成操作2.因为ServiceImpl已经实现了IService下的方法,所以当前服务类没有必要再次实现
思想:共性的业务代码交给框架封装维护,非共性的业务,在接口xxxService定义,然后在当前的服务类下实现
核心API介绍
5.2 快速入门
1.定义服务扩展接口
2.定义服务实现
3.测试
结果:
5.3 MP封装Service实现crud操作
(1)1.条件查询且仅返回一个
/**
* 实现crud
* 1.条件查询且仅返回一个
* getOne: sql查询的结果必须为1个或者没有,否则报错 即符合条件的超过1个就会报错
*/
@Test
public void c(){
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery();
wrapper.le(User::getAge,12);
User one = userService.getOne(wrapper);
System.out.println(one);
}
}
(2)根据条件批量查询
/**
* @Description 根据条件批量查询并分页
*/
@Test
public void test4(){
LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(User.class);
wrapper.gt(User::getAge,20);
//构建分页对象
IPage page=new Page<>(2,3);
userService.page(page,wrapper);
System.out.println(page.getRecords());
System.out.println(page.getPages());
System.out.println(page.getTotal());
}
/**
* @Description 测试服务层save保存单条操作
*/
@Test
public void test5(){
User user1 = User.builder().name("wangwu").userName("laowang4").
email("444@163.com").age(20).password("333").build();
boolean isSuccess = userService.save(user1);
System.out.println(isSuccess?"保存成功":"保存失败");
}
/**
* @Description 测试服务层批量保存
*/
@Test
public void test6(){
User user2 = User.builder().name("wangwu2").userName("laowang2").
email("444@163.com").age(20).password("333").build();
User user3 = User.builder().name("wangwu3").userName("laowang3").
email("444@163.com").age(20).password("333").build();
boolean isSuccess = userService.saveBatch(Arrays.asList(user2, user3));
System.out.println(isSuccess?"保存成功":"保存失败");
}
/**
* @Description 根据id删除操作
*/
@Test
public void test7(){
boolean isSuccess = userService.removeById(17l);
System.out.println(isSuccess?"保存成功":"保存失败");
}
/**
* @Description 根据条件批量删除
*/
@Test
public void test8(){
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class);
wrapper.gt(User::getId,12)
.gt(User::getAge,20);
boolean remove = userService.remove(wrapper);
System.out.println(remove);
}
/**
* @Description 测试根据id更新数据
*/
@Test
public void test9(){
//UPDATE tb_user SET password=?, t_name=? WHERE id=?
User user2 = User.builder().name("wangwu2").password("333").id(3l).build();
boolean success = userService.updateById(user2);
System.out.println(success);
}
/**
* @Description 测试根据条件批量更新
*/
@Test
public void test10(){
LambdaUpdateWrapper<User> wrapper = Wrappers.lambdaUpdate(User.class);
//UPDATE tb_user SET age=? WHERE (id IN (?,?,?))
wrapper.in(User::getId,Arrays.asList(1l,3l,5l)).set(User::getAge,40);
boolean update = userService.update(wrapper);
System.out.println(userService);
}
6.MP代码生成器
前景:新的业务实现时,构建过程重复简单的步骤,效率低。
于是又了MP代码生成器,通过MP代码生成器可以生成模板性的代码,减少手工操作的繁琐,使开发人员聚焦于业务开发之上,提升开发效率;
CodeGenerator 类是MyBatis-Plus 的核心代码生成器类,通过 CodeGenerator 可以
快速生成 Mapper接口、Entity实体类、Mapper XML、Service、Controller 等各个
模块的代码,极大的提升了开发效率。
不推荐使用,稍微麻烦点
推荐使用插件MybatisX
7.逻辑删除
物理删除是deletefrom ,而逻辑删除只是在删除的数据上打标记,并没有实际删除
实际在删除数据时,为了数据留痕,一般选择逻辑删除,也就是为删除表添加逻辑删除字段,通过修改字段状态值来表示数据是否被删除;mp配置:
# 设置mp运行时行为
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台输出sql
global-config:
db-config:
logic-delete-field: deleted # 约定全局删除字段
logic-delete-value: 1 #删除则为1
logic-not-delete-value: 0
调整实体类:
@Data
@NoArgsConstructor//主要用于mybatis底层反射构建user实体类对象
@AllArgsConstructor//主要是lombok基于构建者模式构建对象
@Builder
/**
* 如果变的名称与实体类名称一致,该注解可省略
*/
@TableName("tb_user")
public class User {
//......
@TableLogic//指定逻辑删除字段
private Integer deleted;
}
测试
@Test
public void testDelete(){
//根据id删除
int count = userMapper.deleteById(12l);
System.out.println(count);
}
输出的效果:
但是对应的查询如果不加添加,则删除的无法查询到:
@Test
public void testGetById(){
User user = userMapper.selectById(12l);//查询已经删除的iD12的User
System.out.println(user);
}
//注意观察它的sql语句 追加的条件deleted=0
效果:
逻辑删除本质就是拦截sql,动态追加sql片段
查询 deleted=0
删除: 将sql转化成update操作;
总结:一定要在yaml配置文件中添加全局逻辑删除的字段,在实体类中指定逻辑删除字段
8.乐观锁
说到乐观锁,我们需要先了解事务
事务的操作本是数据库进行控制,为了方便用户进行业务逻辑的操作,spring对事务进行了扩展实现,在spring框架中有两种事务的实现方式,①编程式事务,用户通过自己的代码来控制事务的处理逻辑。②通过注解@Transactional实现,就是声明式事务。一般开发中我们多用通过添加@Transactional注解来进行实现,当添加此注解之后事务的自动功能就会关闭,由spring框架来进行控制。
说白了,事务操作就是AOP思想的体现,当一个方法添加@Transactional注解后,spring会基于此类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法时,如果有事务处理,就会先把事务的自动提交给关系,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交,如果出现任何异常情况,那么直接进行回滚操作,当户也可控制对哪些异常进行回滚操作。
乐观锁就是当前操作者认为在自己操作资源的过程中,其他人操作相同资源的可能性极低,所以无需加锁,而是通过设置一个版本号来加以约束;
悲观锁:排它锁,比如synchronized关键字就是悲观锁,当前线程做操作时,不允许其它线程做操作;
乐观锁:当前线程做操作时,允许其它线程做操作,但是如果其它线程做了操作,则当前操作失败;
乐观锁在数据库中有什么优势?
避免长事务场景锁定数据资源,导致其它线程操作该资源时阻塞,如果阻塞过多,那么导致数据库连接资源耗尽,进而数据库宕机了;
本质上就是在操作前,先获取操作行的version版本号,然后再做前天操作,然后最后再更新这一行,更新时,给sql条件一个判断版本号的sql片段: select version,xxx from user where id=100; version=30 --> 做其他操作(20s);---> update user set xxx,version=version+1 where xxxx and version=30;
使用场景:
1.业务操作周期长,如果业务整个加入事务,导致数据库资源锁定周期过长,性能降低;
2.如果资源争抢过于激烈,会导致失败重试次数过多,导致性能降低;
@Version
private Integer version;
/**
* 注册插件
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//构建mp的插件注册器bean,通过该bean可批量注册多个插件
MybatisPlusInterceptor plusInterceptor = new MybatisPlusInterceptor();
//配置乐观锁拦截器
OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor();
//注册
plusInterceptor.addInnerInterceptor(optimisticLockerInnerInterceptor);
return plusInterceptor;
}
使用mp的乐观锁,需要先自己根据主键id查询用户信息,信息中包含了此时的version数据,然后再更新,更新时会将查询的version值作为更新条件取更新;
9.mp字段自动填充
自动填充是先填充到实体对象中在插入到数据库中,本质上是实体对象对应的属性
引入:create_time和updateTime字段的自动填充
配置类似拦截器等
@Component
public class FillDataHandler implements MetaObjectHandler {
/**
* 插入数据时相关的数据封装
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime",new Date());
metaObject.setValue("updateTime",new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime",new Date());
}
}
测试:
//自动填充测试
@Test
public void tinsert(){
User user = User.builder().name("阿树")
.age(18)
.email("123123")
.password("12345")
.build();
int i = userMapper.insert(user);
System.out.println(i);
}