1、什么是mp,优点是什么?
Mybatis-plus框架,简称mp。mp是mybatis的一种增强工具【只做增强,不做改变】。
mp是java进阶开发的必学框架,企业开发中很多人说的CRUD,码农几乎不是用mp就是在用mp的路上。
优点:简化开发,提高开发效率,简单易上手;
总之,方便DAO层单表操作,不需要写sql;
2、依赖、配置
废话不多说,直接撸代码,mp就是要经常用才会记得,才会熟练。
1)数据库环境准备【mp_db】
2)创建SpringBoot工程,引入MybatisPlus的启动依赖【mybatis-plus-boot-starter】
现在的企业开发中几乎都是用mp-3,所以选择版本的时候注意看。
<!--mybatisplus起步依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
3)yml配置
#mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4)测试【增、删、改、查】
具体如何操作,涉及的开发注解和代码结构比较繁多,拆分成下文。
3、必备代码插件
在开始学习之前,idea编辑器需要装上Mybatis-X这个插件。想必大家多多少少也是听过的或者用过的,这里只针对小白或者还在用eclipse的同学。
idea这个软件非常强大,具备很多的插件,这些插件都是一些大牛呕心沥血依据自己的编码经验开发出来的,并且免费开源,为了方便我们日常的便捷开发。而且现在公司企业的java开发也都是用这个软件,不多说装个idea,点击File->setting->plugins->搜索Mybatis-x,安装后重启idea
这个插件的作用就是依据yml配置和你的数据库源,自动生成基础代码。
4、Mybatis X代码生成器使用
安装好后,怎么使用呢,4张图说明一切
【1】idea编辑器找到DataBase【一般在编辑器的左边】,准备连接数据库,这里我就用mysql举例
【2】 输入数据库端口、账密
host:数据库所在地址,如果在你本地就输入:127.0.0.1或者localhost;如果是在你的服务器上就输入服务器域名;
port:数据库占用的端口
user/password:账户名、密码
输入完成后,点击【Test Connection】,如果出现绿色对勾并显示【Succeeded】表示连接成功
【3】连接成功,选择想要生成代码的表,右击选择Mybatis-x选项,设置说明如下。设置之后,next
【4】接下来的设置建议和我一样
设置生成引擎用mybatis-plus3;
并且生成的代码自动加上lombok注解(@Data);
并且需要生成实体的Model对象;
mapper文件设置不用变,使用插件给你的默认设置
【5】点击finish后,你的dal工程和Resource就会多出这些文件
5、须知注解
@TableName("表名")
位于dal层实体类上,绑定当前实体类和数据库表
@TableId
位于类的成员属性上,绑定当前表的主键,也就是说这个注解在哪个属性上,哪个属性就是主键。通过这个注解还可以通过type属性配置主键的生成策略
@TableField
位于类的成员属性上,指定 java类的属性 和 数据库字段 的映射【属性名和表列名不一致】,所以如果我们要在实体类中加上一些数据库表不存在的字段,那么记得加上这个注解,并且设置exist=false
注意:我所说的实体类,值得是java中对应数据库表的类,不是说所有的类都是实体类。一般开发时,类与类之间区分很严谨(业务类、配置类、工具类、请求参数类、响应类、异常类等等),总之实体类值得就是与数据库表绑定的类
这个注解的作用很强大,里面属性有很多的作用,这里我只列举开发中常用的几个用法,基本上可以掌握这几个用法就足够了。
作用:
【1】当数据库字段和java属性名不一致,起到映射的作用
【2】设置忽略属性
【3】设置自动填充 (详情可看最后)
【4】保存空值(详情可看最后)
@TableName("tb_user") // 指定表名
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
/**
TODO:1 @TableId: 指定当前表的主键
* value: 建立数据库主键字段与实体字段的对应关系
* type: 设置主键生成策略,
* IdType.NONE/ASSIGN_ID:默认情况下使用雪花算法
* IdType.AUTO:使用数据库的主键自增
* IdType.INPUT:由用户自己指定主键大小
* ASSIGN_UUID:使用UUID生成一个全局唯一字符串
*/
@TableId(type = IdType.AUTO)
private Long id;
private String userName;//数据库:user_name 驼峰映射
@TableField(fill = FieldFill.INSERT_UPDATE)
private String password;//数据库:password 名称一样
/*
TODO:2 @TableField("表列名") 指定映射关系
以下情况可以省略:
[1]名称一样
[2]数据库字段使用_分割,实体类属性名使用驼峰名称(自动开启驼峰映射)
*/
@TableField("t_name")
private String name;//数据库里是t_name,必须用注解映射
private Integer age;//数据库:password 名称一样
//TODO:3 @TableField("表列名") 指定增删改查时的忽略属性
@TableField(exist = false)
private String email;
}
6、基本增删改查-Mapper层
6.1 增:insert(T)
//【1】插入操作:insert()
@Test
public void testInsert(){
User user = User.builder().userName("liuyan")
.age(23)
.email("liuyan@163.com")
.name("柳岩")
.password("123456")
.build();
int i=userMapper.insert(user);
if (i>0) {
System.out.println("添加成功!");
}
}
6.2 删:deleteById(int id); deleteBatchIds(List list) deleteByMap(Map map);
//【2】删除操作
//1.根据id删除:deleteById(int i)
@Test
public void testDelete01(){
int id=22;
int i=userMapper.deleteById(id);
if(i>0)
System.out.println("删除成功!");
}
//2.根据id集合删除:deleteBatchIds(List list)
@Test
public void testDelete02(){
ArrayList<Integer> listId = new ArrayList<>();
Collections.addAll(listId,23,24,25);//一次性添加多个元素
int i=userMapper.deleteBatchIds(listId);
if(i>0)
System.out.println("删除成功!");
}
//3.根据map构成条件删除:deleteByMap(Map map)
@Test
public void testDelete03(){
HashMap<String,Object> map = new HashMap<>();
//map集合作为删除条件【键=数组库字段名 值=数组库字段值】
map.put("user_name","liuyan");
int i=userMapper.deleteByMap(map);
if(i>0)
System.out.println("删除成功!");
}
6.3 改:updateById(T)
只更新实体类中存在的数据,如果对应的属性为null,不更新;
/【3】更改操作:updateById
//只更新实体类中存在的数据,如果对应的属性为null,不更新;
@Test
public void testUpdate(){
//根据主键更新
User user=User.builder().id(16L).userName("zhaoqishang").build();
int i=userMapper.updateById(user);
if(i>0)
System.out.println("修改成功!");
else
System.out.println("修改失败!");
}
6.4 查:【分页、逻辑、模糊、排序、限定】
6.4.1 分页查询(无条件):selectPage(Page,null)
【1】配置类:分页拦截器 {在主配置类中设置Bean}
@Configuration
@MapperScan("com.itheima.mapper")
public class MpConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
//1 注册拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//2 指定分页拦截器的类型(相当于之前的插件PageHelp的方言设置)
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
//3 配置
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,-1不受限制
paginationInterceptor.setMaxLimit(-1L);
//4 添加分页拦截器
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
【2】先分页、再查询、最后获取分页信息
@Test
public void testPageSelect(){
//1、设置分页:每页5条,初始展示第1页【先分页再查询】
Page<User> page = new Page<>(1,5);
//2、开始查询; 参数1:页面设置;参数2:条件设置,相当于sql中where语句【此处无条件,null】
Page<User> userPage = userMapper.selectPage(page,null);
//3、获取分页查询后的数据信息
// 获取分页数据信息
List<User> userList = page.getRecords();
for (User user : userList) {
System.out.println(user);
}
// 当前页
long current = page.getCurrent();
System.out.println("当前页数是:"+current);
// 每页显示条数
long size = page.getSize();
System.out.println("每页数据数量:"+size);
// 总条数
long total = page.getTotal();
System.out.println("当前查询数据总数:"+total);
// 总页数
long pages = page.getPages();
System.out.println("总共页数:"+pages);
}
6.4.2 模糊查询:selectList(wrapper) + 逻辑转换符
逻辑转换符:
@Test
public void testQueryWrapper(){
//1、建立QueryWrapper对象 泛型为User
QueryWrapper<User> wrapper = new QueryWrapper<>();
//2、条件筛选
wrapper.like("user_name","伤")
.eq("password","123456")
.in("age",19,25,29)
.orderByDesc("age");
//3、查询所有:selectList()
List<User> users = userMapper.selectList(wrapper);
//4、遍历
for (User user : users) {
System.out.println("user = " + user);
}
}
解析:QueryWrapper对象相当于sql语句中where条件,但是它的限制——
QueryWrapper查询数据时,需要手写对应表的 数据库字段,
一旦之后更改了数据库的列名,也就是表结构更改后,那么对应的代码修改量也会变大,维护性较差
[改进方案]____:
LamQueryWrapper 查询数据时,用实体类的get()方法 代替 数据库字段
例如:
eq("id",18) >> eq(User::getId,18)
in("age",(1,2,3)) >> in(User::getAge,(1,2,3))
//上述查询用LambdaQueryWrapper()完善后
@Test
public void testLambdaQueryWrapper(){
//1、建立QueryWrapper对象 泛型为User
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//2、条件筛选
wrapper.like(User::getName,"伤")
.eq(User::getPassword,"123456")
.in(User::getAge,19,25,29)
.orderByDesc(User::getAge);
//3、查询所有:selectList()
List<User> users = userMapper.selectList(wrapper);
//4、遍历
for (User user : users) {
System.out.println("user = " + user);
}
}
6.4.3 逻辑查询: or -> selectList(Wrapper)
@Test
public void testLambdaQueryWrapper02(){
//1、建立QueryWrapper对象 泛型为User
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//2、条件筛选
wrapper.like(User::getName,"伤")
.or()
.in(User::getAge,19,25,29)
.orderByDesc(User::getAge);
//3、查询所有:selectList()
List<User> users = userMapper.selectList(wrapper);
//4、遍历
for (User user : users) {
System.out.println("user = " + user);
}
}
逻辑转换符or:主动调用or(),说明下一个方法为【或条件】,默认就是and连接!
6.4.4 排序查询: orderByAsc() \ orderByDesc() -> selectList(Wrapper)
@Test
public void testLambdaQueryWrapper03(){
//1、建立QueryWrapper对象 泛型为User
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//2、条件筛选
wrapper.orderByAsc(User::getAge)
.orderByDesc(User::getId);
//3、查询所有:selectList()
List<User> users = userMapper.selectList(wrapper);
//4、遍历
for (User user : users) {
System.out.println("user = " + user);
}
}
6.4.5 限定查询:select() -> selectList(Wrapper)
默认selectList()查询所有字段,Wrapper可以用select限定查询的字段。
@Test
public void testLambdaQueryWrapper04(){
//1、建立QueryWrapper对象 泛型为User
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//2、条件筛选(select:只查询User的Id和Name字段)
wrapper.like(User::getName,"伤")
.or()
.in(User::getAge,19,25,29)
.orderByDesc(User::getAge)
.select(User::getId,User::getName);
//3、查询所有:selectList()
List<User> users = userMapper.selectList(wrapper);
//4、遍历
for (User user : users) {
System.out.println("user = " + user);
}
}
6.4.6 分页条件查询:selectPage(Page,LambdaQueryWrapper)
/*
其实就是第【1】个无条件查询中,
将第二个参数null用wrapper代替了,只不过之前没有讲LambdaQueryWrapper
*/
@Test
public void testLambdaQueryWrapper05(){
//1、建立QueryWrapper对象和Page对象 泛型为User
Page<User> page = new Page<>(2,3);//【先分页再查询】
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//2、条件筛选
wrapper.like(User::getName,"伤")
.or()
.in(User::getAge,19,25,29)
.orderByDesc(User::getAge)
.select(User::getId,User::getName);
//3、分页条件查询:selectPage()
Page<User> userPage = userMapper.selectPage(page,wrapper);
//4、获取分页数据信息:getRecords()
List<User> userList = page.getRecords();
for (User user : userList) {
System.out.println(user);
}
long current = page.getCurrent();
System.out.println("当前页数是:"+current);
// 每页显示条数
long size = page.getSize();
System.out.println("每页数据数量:"+size);
// 总条数
long total = page.getTotal();
System.out.println("当前查询数据总数:"+total);
// 总页数
long pages = page.getPages();
System.out.println("总共页数:"+pages);
}
7、业务增删改查-service层
MP实现Service封装【所有的crud在参数中就已经实现!】 1.定义一个服务扩展接口,该接口继承公共接口IService; 2.定义一个服务实现类,该类继承ServiceImpl<Mapper,Entity>,并实现自定义的扩展接口; 注意事项: 1.ServiceImpl父类已经注入了UserMapper对象,名称叫做baseMapper, 所以当前实现类直接可以使用baseMapper完成操作 2.因为ServiceImpl已经实现了IService下的方法,所以当前服务类没有必要再次实现 主要Api: 增—— save(T):boolean 保存单条操作【增加操作】 saveBatch(Collection<T>):boolean 批量保存操作【批量增加】 删—— removeById(Serializable):boolean 根据id删除 removeByMap(Map<String,Object>):boolean 根据指定条件删除 改—— updateById(T):boolean 根据id更新【如果id不存在,更新失败】 update(Wrapper<T>):boolean 根据wrapper条件更新【wrapper只是筛选条件,要想更新必须set:wrapper.set】 查—— getById(Serializable):T 根据id查询 listByIds(Collection<? extends Serializable> lsit):List<T> 根据id集合查询 getOne(Wrapper<T>):T 根据指定条件,查询符合条件的某一条信息 list(Wrapper<T>):List<T> 根据指定条件,查询所有符合改条件的信息 page(E,Wrapper):E 将E按照条件Wrapper分页查询
目的:
- 开发更加快捷,对业务层也进行了封装Service层注入mapper ;
- 业务层开发时,提供的接口和实现类,编码更加高效
操作步骤:
1.定义一个服务扩展接口,该接口继承公共接口IService;
public interface UserService extends IService<User> {
}
2.定义一个服务实现类,该类继承ServiceImpl<Mapper,Entity>,并实现自定义的扩展接口;
@Service
//继承在前,实现在后
public class UserServiceImpl
extends ServiceImpl<UserMapper, User>
implements UserService{}
使用: 测试类中使用时不需要注入mapper,但是Service。实际应用中,因为业务开发都在Service层中,所以不需要@Autowired注入,只要定义一个Service的对象即可
下面主要Api的测试:
//【1】 save(T):boolean 保存单条操作【增加操作】
@Test
public void testUserService01(){
User user1 = User.builder().name("wangwu1").userName("laowang2").
email("444@163.com").age(20).password("333").build();
boolean isSuccess = userService.save(user1);
System.out.println(isSuccess?"保存成功":"保存失败");
}
//【2】saveBatch(Collection<T>):boolean 批量保存操作【批量增加】
@Test
public void testUserService02(){
User user1 = User.builder().name("wangwu2").userName("laowang2").
email("444@163.com").age(20).password("333").build();
User user2 = User.builder().name("wangwu3").userName("laowang3").
email("444@163.com").age(20).password("333").build();
/*
Arrays.asList(xx1,xx2,xx3)
将同类型的对象型数组变成List集合;
不适用于基本数据类型:int long char String
不支持add()、remove()、clear()等方法
总结:Array.asList()得到的集合,只能用来遍历!!!
*/
boolean isSuccess = userService.saveBatch(Arrays.asList(user1, user2));
System.out.println(isSuccess?"保存成功":"保存失败");
}
//【3】removeById(Serializable):boolean 根据id删除
@Test
public void testUserService03(){
int id=31;
boolean result = userService.removeById(id);
System.out.println("result = " + result);
}
//【4】removeByMap(Map<String,Object>):boolean 根据指定条件删除
@Test
public void testUserService04(){
HashMap<String, Object> map = new HashMap<>();
map.put("id","32");
boolean result = userService.removeByMap(map);
System.out.println("result = " + result);
}
//【5】updateById(T):boolean 根据id更新一条数据【如果id不存在,更新失败】
@Test
public void testUserService05(){
//将id为30的user_name修改为:老王
User user = User.builder().userName("老王").id(30L).build();
boolean result = userService.updateById(user);
System.out.println("result = " + result);
}
//【6】update(Wrapper<T>):boolean 根据wrapper条件批量更新【wrapper.set】
@Test
public void testUserService06(){
//将id为{1,3,5}的age修改为:40 LambdaUpdateWrapper实现
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.in(User::getId,Arrays.asList(1l,3l,5l)).set(User::getAge,40);
boolean result = userService.update(wrapper);
System.out.println("result = " + result);
}
//【7】根据id查询
@Test
public void testUserService07(){
int id=15;
User user = userService.getById(id);
System.out.println("user = " + user);
}
//【8】listByIds(Collection<? extends Serializable> lsit):List<T> 根据id集合批量查询
@Test
public void testUserService08(){
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,1,3,5);
List<User> userList = userService.listByIds(list);
for (User user : userList) {
System.out.println("user = " + user);
}
}
//【9】getOne(Wrapper<T>):T 根据指定条件,查询符合条件的某一条信息
// ***getOne:sql查询的结果必须为1条或者没有,否则报错 !!!!***
@Test
public void testUserService09(){
//查询年龄<19的用户【必须只有一个,如果有多个用户年龄<19,查询报错!!】
// LambdaQueryWrapper实现
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(User::getAge, 19);
User one = userService.getOne(wrapper);
System.out.println(one);
}
//【10】list(Wrapper<T>):List<T> 根据指定条件,查询所有符合改条件的信息
@Test
public void testUserService10(){
//查询id:1~5中,年龄=20的用户
// LambdaQueryWrapper实现
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
ArrayList<Integer> idList = new ArrayList<>();
Collections.addAll(idList,1,2,3,4,5);
wrapper.in(User::getId,idList).eq(User::getAge,20);
List<User> userList = userService.list(wrapper);
for (User user : userList) {
System.out.println("user = " + user);
}
}
//【11】page(E,Wrapper):E 将E按照条件Wrapper分页查询 先分页,在查询
@Test
public void testUserService11(){
//1.Page分页
Page<User> userPage = new Page<>(1, 4);
//2.Wrapper条件筛选
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(User::getAge,35);
//3.查询
Page<User> page = userService.page(userPage, wrapper);
//4.获取分页信息
System.out.println(page.getRecords());//记录内容
System.out.println(page.getPages());//总页数
System.out.println(page.getTotal());//当前查询到的记录数量
}
8、MybatisPlus配置自动填充
应用场景:插入或者更新数据时,希望有些字段可以自动填充数据,比如密码、version等。例如:初始注册的用户,密码默认都是123456 怎么用?【@TableField的fill属性+配置】 【1】编写配置类,实现MetaObjectHandler接口
如图:我想给每个实体类的通用字段create_time、create_by、update_time、update_by自动填充
填充规则:新增【重写insertFill方法】or修改【重写updateFill方法】某条记录的时候
注意:我这里对于create_time、update_time的填充是当前时间;对于create_by、update_by的填充是用自己封装的工具类SecurityUtils来获取当前操作用户的信息进行填充
@Component
public class CommonMetaObjectHandler implements MetaObjectHandler {
// 插入操作时,自动填充
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
this.strictInsertFill(metaObject, "deleteFlag", Boolean.class, Boolean.FALSE);
this.strictInsertFill(metaObject, "createTime", Date.class, now);
this.strictInsertFill(metaObject, "createBy", String.class, SecurityUtils.getUserName());
this.strictInsertFill(metaObject, "updateTime", Date.class, now);
this.strictInsertFill(metaObject, "updateBy", String.class, SecurityUtils.getUserName());
}
// 更新操作时,自动填充
@Override
public void updateFill(MetaObject metaObject) {
Date now = new Date();
// this.strictUpdateFill(metaObject, "updateTime", Date.class, now);
// this.strictUpdateFill(metaObject, "updateBy", String.class, SecurityUtils.getUserName());
this.setFieldValByName("updateTime", now, metaObject);
this.setFieldValByName("updateBy", SecurityUtils.getUserName(), metaObject);
}
}
【2】在实体类的需要自动填充的字段上添加注解: @TableField并设置属性fill,fill本质是一个枚举四种权限———— DEFAULT,默认不处理 INSERT,插入时填充字段 UPDATE,更新时填充字段 INSERT_UPDATE,插入和更新时填充字段【常用!】
9、如何保留空值修改
有时候前端传参的时候,某些字段会给一个空值或者null,但是mybatis修改数据的时候对于null或者为空的字段会进行忽略,这样在修改的时候调用updateById等方法时会忽略到这个条件,导致修改结果不符合预期。
举例说明:
现在有一条记录表示余额的money字段为100,year字段为1;
前端请求传值的时候money为null表示余额清0,year为2,由于前端没有对money传值,那么mybatis在更新只会将year更新为2,而money仍然为100。这显然与结果不符合,那么这个时候就需要在实体类的money字段上添加一个注解来避免这种情况。
【当然,也可以在业务层做阻断,如果前端传null,就初始化为0,这种解决方式也可以】
@TableField(updateStrategy = FieldStrategy.IGNORED)