首页 > 其他分享 >MybatisPlus

MybatisPlus

时间:2024-03-14 20:45:59浏览次数:14  
标签:MybatisPlus private 查询 User id public user

入门

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

准备数据

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user`
(
    id BIGINT NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

DELETE FROM `user`;

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]');

pom

<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.33</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
  
<dependency>  
    <groupId>org.mybatis.spring.boot</groupId>  
    <artifactId>mybatis-spring-boot-starter</artifactId>  
    <version>3.0.3</version>  
</dependency>  
  
<dependency>  
    <groupId>org.projectlombok</groupId>  
    <artifactId>lombok</artifactId>  
</dependency>

controller

@RestController  
public class UserController {  
    @Autowired  
    private UserService userService;  
  
    @GetMapping("/selectAll")  
    public List<User> queryAll(){  
        return userService.queryAll();  
    }  
}

service

@Service  
public class UserServiceImpl implements UserService {  
  
    @Autowired  
    private UserMapper userMapper;  
    @Override  
    public List<User> queryAll() {  
        return userMapper.selectAll();  
    }  
}

mapper

@Mapper  
public interface UserMapper {  
    public List<User> selectAll();  
}
<mapper namespace="com.eun.mapper.UserMapper">  
  
    <select id="selectAll" resultType="com.eun.domain.User">  
        select * from user    
	</select>  
</mapper>

Mybatis开发效率

每当我们使用Mybatis框架的时候,都会有如下步骤:

  1. Mapper接口提供一个抽象方法
  2. Mapper映射文件提供SQL标签和语句
  3. Service中注入Mapper对象
  4. 调用Mapper实例的方法
  5. Controller中注入Service对象
  6. 调用Service实例的方法

有一些操作是通用的:

  1. 对于DAO,可以由框架生成单表的Mapper抽象方法和对应的SQL实现,不需要我们去实现
  2. 对于Service,可以由框架提供一些Service的抽象方法和对应的实现,而不需要我们手动实现

MybatisPlus是对Mybatis的简化和封装。

使用MybatisPlus

两步:

  1. 引入依赖
  2. 继承BaseMapper,指定泛型为实体类
  3. 实体类添加注解

引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.2</version>
</dependency>

MybatisPlus提供了starter,引入该依赖会传递入Mybatis依赖,并且完成了对Mybatis的自动装配,需要替换掉原有的Mybatis依赖(避免冲突)

    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>
    </dependencies>

继承BaseMapper

image-20231205114336592 | 650

继承BaseMapper<T>,指定泛型:

public interface UserMapper extends BaseMapper<User> {  

}

注意:没有指定@Mapper

测试:

@SpringBootTest  
class UserMapperTest {  
  
    @Autowired  
    private UserMapper userMapper;  
  
    @Test  
    void testInsert() {  
        User user = new User();  
        user.setId(5L);  
        user.setUsername("Lucy");  
        user.setPassword("123");  
        user.setPhone("18688990011");  
        user.setBalance(200);  
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");  
        user.setCreateTime(LocalDateTime.now());  
        user.setUpdateTime(LocalDateTime.now());  
        userMapper.insert(user);  
    }
}

没有指定@Mapper注解,也可以注入UserMapper,因为在启动类上使用了注解:

@MapperScan("com.itheima.mp.mapper")  
@SpringBootApplication  
public class MpDemoApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(MpDemoApplication.class, args);  
    }  
  
}

指定Mapper的扫描包,生成代理对象

常用注解

在上文的程序中,基于MybatisPlus我们并没有在Mapper接口中写SQL语句,也没有指定Mapper映射文件,MybatisPlus是如何知道我们要操作哪张表的?

public interface UserMapper extends BaseMapper<User> {  

}

在UserMapper接口中,继承BaseMapper时指定了泛型User,MybatisPlus自动将User的首字母变为小写得到user作为数据库的表名

  • MybatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息

image.png | 550

  1. 类名 驼峰 转 下划线 得到表名
  2. 名为id的字段作为主键
  3. 变量名 驼峰 转 下划线 作为字段名

但是实际开发中表名很多都是以tb_开始的,和实体类的名字对应不上,需要通过MybatisPlus提供的注解来指定别名:

  • @TableName:指定表名
  • @TableId:指定表的主键字段信息
  • @TableField:指定表中的普通字段信息

@TableId

@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})  
public @interface TableId {  
  
    /**  
     * 字段名(该值可无)  
     */  
    String value() default "";  
  
    /**  
     * 主键类型  
     * {@link IdType}  
     */    
     IdType type() default IdType.NONE;  
}

value():指定主键字段名

type():指定主键类型,类型为IdType枚举:

public enum IdType {  
    AUTO(0),  
    NONE(1),  
    INPUT(2),  
    ASSIGN_ID(3),  
    ASSIGN_UUID(4);  
  
    private final int key;  
  
    private IdType(int key) {  
        this.key = key;  
    }  
  
    public int getKey() {  
        return this.key;  
    }  
}
含义
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)
ID_WORKER 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID)
UUID 32 位 UUID 字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)

这里比较常见的有三种:

  • AUTO:利用数据库的id自增长

  • INPUT:手动生成id

  • ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略

@TableField

使用场景

image.png

  • 成员变量名和数据库字段名不一致

实体类的实例变量名是name,而数据库表的字段名为username,需要使用@TableField指定字段名

  • 实例变量以is开头,并且是布尔值

MybatisPlus识别字段时默认映射的数据库表字段名为married,但是数据库表的字段名实际上就是is_married

  • 实例变量名和数据库关键字冲突

实例变量名 order 和SQL语法中排序关键字 order 冲突,需要使用 反引号 进行转义

  • 实例变量不是数据库字段

实例变量address在数据库表中并没有这个字段,在查询操作时通过反射获取到实例变量名,再从ResultSet中获取这个字段值时就会出错

常用配置

MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:使用配置 | MyBatis-Plus

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包

  • 全局id类型

image.png | 600

mybatis-plus:  
  type-aliases-package: com.itheima.domain.po  
  mapper-locations: classpath*:mapper/*.xml  
  configuration:  
    cache-enabled: false  
  global-config:  
    db-config:  
      id-type: auto  
      update-strategy: not_null #只更新非空值

mapper-locations:Mybatis-Plus支持自定义Mapper映射文件,此属性指定mapper的扫描位置,默认就是这个值,也可以不指定,只要mapper映射文件在这个位置一定会被扫描

classpath* 其中的 * 包含是扫描其他jar包下的配置文件

id-type:指定主键id的类型,此处指定后在 @TableId 中就不必再指定了,如果指定就会覆盖全局的id-type

指定雪花算法后再指定id递增就会在雪花的基础上进行递增

update-strategy:指定update操作时的策略

核心功能

条件构造器

通用Mapper提供了根据复杂条件Wrapper进行CRUD的方法

image.png

selectOne:Ajax查询用户名是否重复

Wrapper继承结构:

image.png

Wrapper相当于where条件,实际上CRUD都是根据where条件来进行的,只是复杂更新会使用UpdateWrapper

AbstractWrapper:提供了where中的所有条件

image.png | 550

QueryMapper:在AbstractWrapper的基础上扩展了select方法,可以指定要查询的字段

image.png

select(...):指定要查询的字段,默认是 select *

UpdateWrapper:在AbstractWrapper的基础上扩展了set方法,可以指定SQL语句中set部分的操作

image.png

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件,这些操作都需要where匹配到指定的数据,而复杂的更新需要使用UpdateWrapper来完成

案例

QueryWrapper

  • 查询出名字带有 o 的,存款 ≥ 1000元的记录的id、username、info、balance
select
	id,username,info,balance
from user
where username like '%o%' and balance >= ?
@Autowired  
private UserMapper userMapper;  
  
@Test  
public void testQueryWrapper(){  
    QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();  
    userQueryWrapper.select("id","username","info","balance")  
                    .like("username","o")  
                    .ge("balance",1000);  
    List<User> userList = userMapper.selectList(userQueryWrapper);  
    for (User user : userList) {  
        System.out.println(user);  
    }  
}
  • 更新用户名为jack的用户的余额为2000
//更新用户名为jack的用户的余额为 2000@Test  
public void testQueryWrapper2(){  
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
    queryWrapper.eq("username","jack");  
    userMapper.update(User.builder().balance(2000).build(),queryWrapper);  
}

此时QueryWrapper作为where查询条件,也可以使用UpdateWrapper作为查询条件:

@Test  
public void testQueryWrapper3() throws Exception {  
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>()  
            .eq("username", "jack")  
            //设置余额为2000  
            .set("balance", 2000);  
    userMapper.update(null,userUpdateWrapper);  
}

使用UpdateWrapper时,update方法第一个参数可以指定为null,在第二个参数UpdateWrapper中通过set指定要更新的值

UpdateWrapper

基于BaseMapper的update方法使用QueryWrapper更新时只能直接赋值,对于一些复杂的需求就难以实现,需要使用UpdateWrapper

  • 需求:更新id为1,2,4的用户的余额,扣200
update user set balance = balance - 200 where id in (1,2,4)

此时的SET进行赋值操作需要基于字段现有值,此时就要使用UpdateWrapper的setSql功能:

@Test  
public void testQueryWrapper4() throws Exception {  
  
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>();  
    userUpdateWrapper.setSql("balance = balance - 200")  
                    .in("id",1,2,4);  
    userMapper.update(null,userUpdateWrapper);  
}

LambdaQueryWrapper

在UpdateWrapper或QueryWrapper中,数据库表的字段名都是字符串魔法值,这在编码规范中显然是不推荐的,可以使用LambdaQueryWrapper

LambdaQueryWrapper就是基于变量的getter方法结合反射技术,只需要将条件对应的字段的getter方法传递给MybatisPlus,就能计算出对应的变量名了。传递方法可以使用JAVA8的方法引用和Lambda表达式,MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:

  • LambdaQueryWrapper对应QueryWrapper

  • LambdaUpdateWrapper对应UpdateWrapper

  • 查询出名字带有 o 的,存款 ≥ 1000元的记录的id、username、info、balance

@Test  
public void testLambdaQueryWrapper() throws Exception {  
    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();  
    lambdaQueryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance)  
            .ge(User::getBalance, 1000);  
    List<User> userList = userMapper.selectList(lambdaQueryWrapper);  
    userList.forEach(System.out::println);  
}

如果要改变数据库表的字段名,只需要shift + F6改变对应po类的属性名,所有使用get方法的地方都会随之改变

但是此时查询会报错:

Caused by: java.lang.IndexOutOfBoundsException: Index 4 out of bounds for length 4

数组下标越界,关键信息是数组长度为4,我们查询的结果是4个字段,在上文中的更新案例中:

//更新用户名为jack的用户的余额为 2000@Test  
public void testQueryWrapper2(){  
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
    queryWrapper.eq("username","jack");  
    userMapper.update(User.builder().balance(2000).build(),queryWrapper);  
}

将User类添加了@Builder注解,这个注解会在User类中生成全参构造,没有无参构造了,但是MybatisPlus查询结果集映射时使用的是无参构造,需要添加@NoArgsConstructor,@AllArgsConstructor:

@Data  
@TableName("user")  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
public class User {  
  
    /**  
     * 用户id  
     */  
    @TableId(value = "id")  
    private Long id;  
  
    /**  
     * 用户名  
     */  
    private String username;  
  
    /**  
     * 密码  
     */  
    private String password;  
  
    /**  
     * 注册手机号  
     */  
    private String phone;  
  
    /**  
     * 详细信息  
     */  
    private String info;  
  
    /**  
     * 使用状态(1正常 2冻结)  
     */  
    private Integer status;  
  
    /**  
     * 账户余额  
     */  
    private Integer balance;  
  
    /**  
     * 创建时间  
     */  
    private LocalDateTime createTime;  
  
    /**  
     * 更新时间  
     */  
    private LocalDateTime updateTime;  
}

自定义拼接SQL

在更新余额时,我们在service中写了SQL语句:

@Test  
public void testQueryWrapper4() throws Exception {  
  
    UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>();  
    userUpdateWrapper.setSql("balance = balance - 200")  
                    .in("id",1,2,4);  
    userMapper.update(null,userUpdateWrapper);  
}

这种写法也是不太规范的,SQL语句最好都维护在持久层,而不是业务层。对于当前案例来说,由于是批量更新,只能将SQL写在Mapper.xml文件中,使用foreach生成动态SQL,如果查询条件非常复杂,SQL语句也会变得更加复杂

MybatisPlus提供了自定义SQL的功能,利用Wrapper生成查询条件,结合Mapper.xml拼接SQL

@Test
public void testCustomerWrapper(){
	//利用Wrapper构造查询条件
	QueryWrapper<User> queryWrapper = new QueryWrapper();
	queryWrapper.in("id",1,2,4);
	//调用自定义的更新方法,传入更新数值和Wrapper对象
	userMapper.updateBalance(200,queryWrapper);
}

@Test  
public void testCustomerWrapper(){  
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();  
    queryWrapper.in(User::getId,1,2,4);  
    int amount = 200;  
    userMapper.updateBalanceByIds(amount,queryWrapper);  
}

对应的Mapper

@Update("update user set balance = balance - #{count} ${ew.customSqlSegment}")
public void updateBalance(@Param("count") int count, @Param("ew") QueryWrapper<User> queryWrapper);

注意:上述语句中的ew和customerSqlSegment都不能修改

  1. queryWrapper查询条件对象相当于SQL语句的where子句

  2. ${ew.customSqlSegment}可以用在注解中,也可以使用在Mapper.xml文件中对SQL语句进行拼接

Service接口

MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的Service模板方法,通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以大致分为几类:

  • save 新增
  • remove 删除
  • update 更新
  • get 查询单个结果
  • list 查询结果集
  • count 计数
  • page 分页

基本方法说明

image.png

新增

image-20231206103039651

  • save是新增单个元素

  • saveBatch是批量新增

  • saveOrUpdate根据id判断如果数据存在就更新,不存在则新增

  • saveOrUpdateBatch是批量的新增或修改

删除

image-20231206103321161

  • removeById:根据id删除

  • removeByIds:根据id批量删除

  • removeByMap:根据Map中的键值对为条件删除

  • remove(Wrapper):根据Wrapper条件删除

修改

![image-20231206104024837](file:///D:/Desktop/%E8%AF%BE%E5%A0%82%E6%96%87%E4%BB%B6/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%98%B6%E6%AE%B5/01-mybatisplus/%E8%AE%B2%E4%B9%89/assets/image-20231206104024837.png?lastModify=1706277492)

  • updateById:根据id修改

  • update(Wrapper):根据UpdateWrapper修改,Wrapper中包含setwhere部分

  • update(T,Wrapper)按照T内的数据修改与Wrapper匹配到的数据

  • updateBatchById:根据id批量修改

Get

image-20231206104226471

  • getById:根据id查询1条数据

  • getOne(Wrapper):根据Wrapper查询1条数据

  • getBaseMapper获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

List

image-20231206104321627

  • listByIds:根据id批量查询

  • list(Wrapper):根据Wrapper条件查询多条数据

  • list():查询所有

Count

image-20231206104356450

  • count():统计所有数量

  • count(Wrapper):统计符合Wrapper条件的数据数量

lambda

image.png

  • lambdaQuery()

  • lambdaUpdate()

上文中我们进行LambdaQueryWrapper时需要手动new这个对象,但是IService提供了方法可以直接获取

使用IService

image.png | 550

  1. 自定义Service接口继承IService接口,指定泛型 本次操作的实体类
public interface IUserService extends IService<User> {  
  
}
  1. 自定义Service实现类,实现自定义接口并继承ServiceImpl实现类,指定泛型:操作的Mapper和实体类User
@Service  
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {  

}

案例

image.png

分析:前四个接口在IService中都有实现,而最后一个需要自己在Service中实现

  1. 引入web和knife4j依赖:
        <!--swagger-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
            <version>4.1.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

然后需要在 application.yaml 配置swagger信息如下:

knife4j:
  enable: true
  openapi:
    title: 用户管理接口文档
    description: 用户管理接口文档
    version: 1.0
    concat: 黑马
    url: http://www.itheima.com
    email: [email protected]
    group:
      default:
        group-name: default
        api-rule: package
        #指定扫描的包
        api-rule-resources:   
          - com.itheima.mp.controller
  1. 准备DTO、VO

  1. 开发
@Api(tags = "用户管理接口")  
@RestController  
@RequestMapping("/users")  
@RequiredArgsConstructor  
public class UserController {  
  
    private final IUserService iUserService;  
  
    @PostMapping  
    @ApiOperation("新增用户")  
    public void create(@RequestBody UserFormDTO userFormDTO){  
        iUserService.save(BeanUtil.copyProperties(userFormDTO, User.class));  
    }  
  
    @DeleteMapping("/{id}")  
    @ApiOperation("根据id删除用户")  
    public void remove(@PathVariable Long id){  
        iUserService.removeById(id);  
    }  
  
    @GetMapping("/{id}")  
    @ApiOperation("根据id查询用户")  
    public UserVO queryById(@PathVariable Long id){  
        return BeanUtil.copyProperties(iUserService.getById(id),UserVO.class);  
    }  
  
    @GetMapping()  
    @ApiOperation("根据ids批量查询用户")  
    public List<UserVO> queryByIds(@RequestParam("ids")List<Long> ids){  
        return BeanUtil.copyToList(iUserService.listByIds(ids),UserVO.class);  
    }  
  
}

使用了IUserServiceImpl中已经实现的方法。

注意:此时没有使用@AutoWired注入,使用的是构造方法注入,需要指定@RequiredArgsConstructor注解

自定义Service方法

image.png

根据id更新扣减余额在IUserServiceImpl中没有该方法的实现,需要在IUserService中定义该方法。

controller

@ApiOperation("根据id扣减余额")  
@PutMapping("/{id}/deduction/{amount}")  
public void updateBalance(@PathVariable Long id, @PathVariable Integer amount){  
    iUserService.deductionBalance(id,amount);  
    return ;  
}

service

  • 使用IService实现的updateById方法
@Service  
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {  
  
    @Override  
    public void deductionBalance(Long id, Integer amount) {  
        //1. 查询用户  
        User user = this.getById(id);  
        //2. 判断用户是否存在及状态是否正常  
        if (ObjectUtil.isNull(user) || user.getStatus().equals(2)){  
            return;  
        }  
        //3. 判断金额是否充足  
        if (user.getBalance() < amount){  
            return;  
        }  
        //4. 扣减余额  
        user.setBalance(user.getBalance() - amount);  
        this.updateById(user);  
    }  
}
  • 使用UserMapper进行更新

使用UserMapper进行更新就需要先获取到mapper对象,调用mapper对象的自定义方法:

mapper:

@Update("update user set balance = balance - #{amount} where id = #{id}")  
void updateBalanceById(Long id, Integer amount);

service

@Service  
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {  
   
    @Override  
    public void deductionBalance(Long id, Integer amount) {  
        //1. 查询用户  
        User user = this.getById(id);  
        //2. 判断用户是否存在及状态是否正常  
        if (ObjectUtil.isNull(user) || user.getStatus().equals(2)){  
            return;  
        }  
        //3. 判断金额是否充足  
        if (user.getBalance() < amount){  
            return;  
        }  
        //4. 扣减余额  
        baseMapper.updateBalanceById(id,amount);  
    }  
}

但是在Service中并未注入baseMapper,这个属性是在父类ServiceImpl中定义的:

@SuppressWarnings("unchecked")  
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {  
  
    protected Log log = LogFactory.getLog(getClass());  
  
    @Autowired  
    protected M baseMapper;  
  
    @Override  
    public M getBaseMapper() {  
        return baseMapper;  
    }  
  
    protected Class<T> entityClass = currentModelClass();  
  
    @Override  
    public Class<T> getEntityClass() {  
        return entityClass;  
    }  
  
    protected Class<M> mapperClass = currentMapperClass();
}

在继承时指定了ServiceImpl的第一个泛型M,也就是其中实例变量baseMapper的类型M

IService的Lambda查询

IService提供了LambdaWrapper功能进行复杂查询。

根据复杂条件查询用户

查询条件如下:

  • name:用户名关键字,可以为空

  • status:用户状态,可以为空

  • minBalance:最小余额,可以为空

  • maxBalance:最大余额,可以为空

类似一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断

@PostMapping("/list")  
@ApiOperation("根据条件UserQuery查询用户列表")  
public List<UserVO> queryList(@RequestBody UserQuery userQuery){  
    LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();  
    
    userLambdaQueryWrapper.like(StrUtil.isNotEmpty(userQuery.getName()), User::getUsername, userQuery.getName())  
            .eq(ObjectUtil.isNotEmpty(userQuery.getStatus()), User::getStatus, userQuery.getStatus())  
            .ge(ObjectUtil.isNotEmpty(userQuery.getMinBalance()), User::getBalance, userQuery.getMinBalance())  
            .le(ObjectUtil.isNotEmpty(userQuery.getMaxBalance()), User::getBalance, userQuery.getMaxBalance());  
            
    List<User> list = iUserService.list(userLambdaQueryWrapper);  
    return BeanUtil.copyToList(list,UserVO.class);  
}

在组织查询条件的时候,加入了 status != null 这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的标签。这样就实现了动态查询条件效果了。

image.png | 550

这里需要手动new LambdaQueryWrapper对象,IService其实提供了lambdaQuery()方法

Service中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法:

@PostMapping("/list")  
@ApiOperation("根据条件UserQuery查询用户列表")  
public List<UserVO> queryList(@RequestBody UserQuery userQuery){  
    List<User> list = iUserService.lambdaQuery().like(StrUtil.isNotEmpty(userQuery.getName()), User::getUsername, userQuery.getName())  
            .eq(ObjectUtil.isNotEmpty(userQuery.getStatus()), User::getStatus, userQuery.getStatus())  
            .ge(ObjectUtil.isNotEmpty(userQuery.getMinBalance()), User::getBalance, userQuery.getMinBalance())  
            .le(ObjectUtil.isNotEmpty(userQuery.getMaxBalance()), User::getBalance, userQuery.getMaxBalance())  
            .list();  
    return BeanUtil.copyToList(list,UserVO.class);  
}

可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:

  • .one():最多1个结果

  • .list():返回集合结果

  • .count():返回计数结果

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。

IService的Lambda更新

与lambdaQuery类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务

需求:改造UserServiceImpl中 根据id修改用户余额的接口,要求如下:

  • 完成对用户状态的校验

  • 完成对用户余额的校验

  • 扣减后余额为0,将用户status置为2(冻结)

也就是:在扣减用户余额后,对用户余额进行判断,如果剩余余额为0,应该将status置为2,这部分是动态SQL,需要使用LambdaUpdateWrapper

  • lambdaUpdate():
@Service  
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {  
  
  
    @Override  
    public void deductionBalance(Long id, Integer amount) {  
        //1. 查询用户  
        User user = this.getById(id);  
        //2. 判断用户是否存在及状态是否正常  
        if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {  
            return;  
        }  
        //3. 判断金额是否充足  
        if (user.getBalance() < amount) {  
            return;  
        }  
        //4. 扣减余额  
        user.setBalance(user.getBalance() - amount);  
  
        this.lambdaUpdate().set(User::getBalance, user.getBalance())  
                .set(user.getBalance() == 0, User::getStatus, 2)  
                .eq(User::getId, user.getId())  
                .update();  
    }  
}
  • LambdaUpdateWrapper
@Service  
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {  
  
  
    @Override  
    public void deductionBalance(Long id, Integer amount) {  
        //1. 查询用户  
        User user = this.getById(id);  
        //2. 判断用户是否存在及状态是否正常  
        if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {  
            return;  
        }  
        //3. 判断金额是否充足  
        if (user.getBalance() < amount) {  
            return;  
        }  
        //4. 扣减余额  
        user.setBalance(user.getBalance() - amount);  

        LambdaUpdateWrapper<User> userLambdaUpdateWrapper = new LambdaUpdateWrapper<>();  
        userLambdaUpdateWrapper.set(User::getBalance, user.getBalance())  
                .set(user.getBalance() == 0, User::getStatus, 2)  
                .eq(User::getId, user.getId());  
        baseMapper.update(null,userLambdaUpdateWrapper);  
    }  
}

使用IService提供的lambdaUpdate方法可以不获取baseMapper;使用LambdaUpdateWrapper就必须传入mapper对象的方法

但是此时是没有开启事务的,如果查询余额后判断充足,但是被其他线程修改了余额,可能会更新为负数,显然是不合适的。

解决办法:

  • for updated添加悲观锁

  • 添加乐观锁

    @Override  
    public void deductionBalance(Long id, Integer amount) {  
        //1. 查询用户  
        User user = this.getById(id);  
        //2. 判断用户是否存在及状态是否正常  
        if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {  
            return;  
        }  
        //3. 判断金额是否充足  
        if (user.getBalance() < amount) {  
            return;  
        }  
        //4. 扣减余额  
        int remainBalance = user.getBalance() - amount;  
  
        this.lambdaUpdate()  
                .set(User::getBalance, remainBalance)  
                .set(user.getBalance() == 0, User::getStatus, 2)  
                .eq(User::getId, user.getId())  
                .eq(User::getBalance,user.getBalance()) //乐观锁  
                .update();  
  
	}

乐观锁:查询时余额在user对象的balance属性中,更新时判断:当前数据库表中的余额和对象的balance相等,如果相等就是这个余额未被修改过

注意:此时就不能 user.setBalance(user.getBalance() - amount) 了,因为要记录刚查询到结果时的余额

IService批量新增

需求:批量插入10w条用户数据,进行对比

  • 普通for循环
  • IService的批量插入
  • 开启rewriteBatchedStatements=true

普通for循环

执行了78117ms

@Test  
public void saveOneByOne(){  
    long begin = System.currentTimeMillis();  
    for (int i = 0; i < 10; i++) {  
        for (int j = 0; j < 10000; j++) {  
            iService.save(buildUser(i * 10000 + j));  
        }  
    }  
    long end = System.currentTimeMillis();  
    //78117ms  
    System.out.println((end - begin) + "ms");  
}  
  
public User buildUser(int i){  
    User user = new User();  
    user.setUsername("user_" + i);  
    user.setPassword("123");  
    user.setPhone("18688990011");  
    user.setBalance(200);  
    user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");  
    user.setCreateTime(LocalDateTime.now());  
    user.setUpdateTime(LocalDateTime.now());  
    return user;  
}

IService批量插入

执行了8822ms

@Test  
public void saveBatch(){  
    long begin = System.currentTimeMillis();  
    List<User> users = new ArrayList<>(10000);  
    for (int i = 0; i < 10; i++) {  
        for (int j = 0; j < 10000; j++) {  
            users.add(buildUser(i * 10000 + j));  
        }  
        iService.saveBatch(users);  
        users.clear();  
    }  
    long end = System.currentTimeMillis();  
    //8822ms  
    System.out.println((end - begin) + "ms");  
}

开启rewriteBatchedStatements

url: jdbc:mysql://127.0.0.1:3306/mp2?useUnicode=true&characterEncoding=UTF-8&
autoReconnect=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai

执行了3413ms

@Test  
public void saveBatch(){  
    long begin = System.currentTimeMillis();  
    List<User> users = new ArrayList<>(10000);  
    for (int i = 0; i < 10; i++) {  
        for (int j = 0; j < 10000; j++) {  
            users.add(buildUser(i * 10000 + j));  
        }  
        iService.saveBatch(users);  
        users.clear();  
    }  
    long end = System.currentTimeMillis();  
    //3413ms  
    System.out.println((end - begin) + "ms");  
}

总结

  • 实现IService接口:单表的基础业务不需要编写代码

  • IService接口提供lambda方法:不必获取mapper再执行SQL,编码更方便

扩展功能

代码生成

使用MybatisPlus之后,基础的Mapper、Service、PO代码相对固定,MybatisPlus官方提供了代码生成器,根据数据库表结构生成相关代码

image.png

方式一:在Idea的plugins市场中搜索并安装MyBatisPlus插件(插件不太稳定,建议按照官网方式)

image-20231219164729208

点击上述的 Config Database 配置数据库连接如下:

image-20231219165012532 | 550

点击 Code Generator 配置代码生成信息如下:

image-20231219165832246

方式二:上述的图形界面插件,存在不稳定因素;所以建议使用代码方式生成。官网安装说明。在项目中 pom.xml 添加依赖如下:

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.32</version>
            <scope>test</scope>
        </dependency>

使用方式:创建MybatisPlusGeneratorTest.java

package com.itheima.mp;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.sql.Types;
import java.util.Collections;

public class MybatisPlusGeneratorTest {

    public static void main(String[] args) {
        String url = "jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
        FastAutoGenerator.create(url , "root", "root")
                .globalConfig(builder -> {
                    builder.author("JBL") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .outputDir("D:\\itcast\\generatedCode"); // 指定输出目录
                })
                .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
                    int typeCode = metaInfo.getJdbcType().TYPE_CODE;
                    if (typeCode == Types.SMALLINT) {
                        // 自定义类型转换
                        return DbColumnType.INTEGER;
                    }
                    return typeRegistry.getColumnType(metaInfo);

                }))
                .packageConfig(builder -> {
                    builder.parent("com.itheima.mp") // 设置父包名
                            .controller("controller")
                            .entity("domain.po") // 设置实体类包名
                           .service("service") // 设置service包名
                           .serviceImpl("service.impl") // 设置service实现类包名
                           .mapper("mapper") // 设置mapper包名
                            //.moduleName("address") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "D:\\itcast\\generatedCode\\mapper")); //设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("address") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_") // 设置过滤表前缀
                            .controllerBuilder().enableRestStyle() // 开启restful风格控制器
                            .enableFileOverride() // 覆盖已生成文件
                            .entityBuilder().enableLombok(); // 开启lombok模型,默认是false
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

  • 注意:输出目录指定到src/main/java

  • 这个方法需要放在Test目录下

静态工具类DB

枚举类型处理器

User类中有一个状态字段:

private Integer status;

类似于这种数据字典的字段一般都会定义为枚举,做业务判断时就可以直接基于枚举进行比较。但是我们数据库采用的是int类型,对应的PO也是Integer类型,在业务操作时必须手动将枚举和Integer进行转换,非常麻烦

MybatisPlus提供了一个处理枚举的类型转换器,可以帮助我们将枚举类型和数据库类型自动转换

image.png

  • @EnumValue:枚举中哪个属性值作为存入数据库的值

  • @JsonValue:枚举中哪个属性值作为序列化时展示的字段

使用步骤

  1. application.yml配置全局枚举处理器
mybatis-plus:  
  type-aliases-package: com.itheima.domain.po  
  mapper-locations: classpath*:mapper/*.xml  
  configuration:  
    cache-enabled: false  
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler  
  global-config:  
    db-config:  
      id-type: auto  
      update-strategy: not_null #只更新非空值
  1. 创建枚举类,修改属性类型
public enum UserStatus {  
    NORMAL(1,"正常"),  
    FREEZE(2,"冻结");  
  
    @EnumValue  //存入数据库的值  
    private int code;  
    @JsonValue  //序列化时显式转换的值  
    private String desc;  
  
    UserStatus(int code, String desc) {  
        this.code = code;  
        this.desc = desc;  
    }  
}

注意:枚举类没有提供Getter方法

JSON类型处理器

当前数据库表中有字段类型就是JSON:

create table address  
(  
    id         bigint auto_increment  
        primary key,  
    user_id    bigint           null comment '用户ID',  
    province   varchar(10)      null comment '省',  
    city       varchar(10)      null comment '市',  
    town       varchar(10)      null comment '县/区',  
    mobile     varchar(255)     null comment '手机',  
    street     varchar(255)     null comment '详细地址',  
    contact    varchar(255)     null comment '联系人',  
    is_default bit default b'0' null comment '是否是默认 1默认 0否',  
    notes      varchar(255)     null comment '备注',  
    deleted    bit default b'0' null comment '逻辑删除'  
)

当前我们存储的都是字符串,前端展示的效果是:

{
  "id": 2,
  "username": "Rose",
  "info": "{\"age\": 19, \"intro\": \"青涩少女\", \"gender\": \"female\"}",
  "status": 1,
  "balance": 0,
  "addressVO": [
    {
      "id": 59,
      "userId": 2,
      "province": "北京",
      "city": "北京",
      "town": "朝阳区",
      "mobile": "13900112222",
      "street": "金燕龙办公楼",
      "contact": "Rose",
      "isDefault": true,
      "notes": null
    },
    {
      "id": 63,
      "userId": 2,
      "province": "广东",
      "city": "佛山",
      "town": "永春",
      "mobile": "13301212233",
      "street": "永春武馆",
      "contact": "Rose",
      "isDefault": false,
      "notes": null
    }
  ]
}

此时数据库的info字段中存储的是一个字符串 {\"age\": 19, \"intro\": \"青涩少女\", \"gender\": \"female\"},PO/VO中也是以字符串类型接收的,但是前端希望得到的就是一个JSON类型的对象

从数据库中查询时就要将这个JSON格式的字符串转换为对象

  1. 修改User的属性类型,并且指定类型处理器和自动映射
@Data  
@TableName(value = "user",autoResultMap = true)  //自动映射
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
public class User {  
  
    @TableId(value = "id")  
    private Long id;  

    private String username;  
  
    private String password;  
  
    private String phone;  
  
    @TableField(typeHandler = JacksonTypeHandler.class)  //类型处理器
    private UserInfo info;  
  
    private UserStatus status;  
}
@Data  
@ApiModel(description = "用户VO实体")  
public class UserVO {  
  
    @ApiModelProperty("用户id")  
    private Long id;  
  
    @ApiModelProperty("用户名")  
    private String username;  
  
    @ApiModelProperty("详细信息")  
    private UserInfo info;  
  
    @ApiModelProperty("使用状态(1正常 2冻结)")  
    private Integer status;  
  
    @ApiModelProperty("账户余额")  
    private Integer balance;  
  
    private List<AddressVO> addressVO;  
}

UserVo中不需要指定类型处理器,只需要改变数据类型为UserInfo,因为此处的属性是直接拷贝的

此时的Info就是一个对象了:

  "id": 2,
  "username": "Rose",
  "info": {
    "age": 19,
    "intro": "青涩少女",
    "gender": "female"
  },
  "status": 1,
  "balance": 0,
  "addressVO": [
    {
      "id": 59,
      "userId": 2,
      "province": "北京",
      "city": "北京",
      "town": "朝阳区",
      "mobile": "13900112222",
      "street": "金燕龙办公楼",
      "contact": "Rose",
      "isDefault": true,
      "notes": null
    },
    {
      "id": 63,
      "userId": 2,
      "province": "广东",
      "city": "佛山",
      "town": "永春",
      "mobile": "13301212233",
      "street": "永春武馆",
      "contact": "Rose",
      "isDefault": false,
      "notes": null
    }
  ]
}

注意:UserMapper.xml 如果有内容的话;那么该文件改名字为 UserMapper.xml.bak , 原有的xml文件中并没有配置json类型的处理器,所以会报错。如果要xml文件中也需要配置,参考:字段类型处理器 | MyBatis-Plus (baomidou.com)

商城类项目的规格就是使用JSON进行存储,这样在数据库中存储这个对象的规格就是任意的

MybatisPlus插件

MybatisPlus提供的内置拦截器:

image.png

  • 多租户插件:在每次查询时拼接一个租户id

  • 分页插件:ThreadLocal中存储一个标记,有标记就进行分页

分页插件

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效,必须配置分页插件。

配置分页插件

@Configuration
public class MybatisConfig {

    //配置Mybatis plus分页拦截器
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

分页API

image.png

Page对象是IPage接口的子类:

image.png

使用:

image.png

测试

@Test  
public void testByPage(){  
    //构造分页对象  
    int pageNo = 2;  
    int pageSize = 3;  
    Page<User> userPage = Page.of(pageNo, pageSize);  
    //排序  
    userPage.addOrder(OrderItem.asc("balance"));  
      
      
    //分页查询  
    Page<User> result = iUserService.page(userPage, null);  
  
    System.out.println(result.getPages());  
    System.out.println(result.getTotal());  
    System.out.println(result.getRecords());  
}

在同一个线程中,多次查询也是会进行分页的,之前的PageHelper只能分页一次

案例-黑马商城

启动nginx不要双击,使用cmd,start nginx.exe

标签:MybatisPlus,private,查询,User,id,public,user
From: https://www.cnblogs.com/euneirophran/p/18073910

相关文章

  • MyBatisPlus代码生成器(新)
    MyBatisPlus代码生成器(新)注意:适用版本:mybatis-plus-generator3.5.1以上版本参考:官网本次配置:JDK17+SpringBoot3.1.5+MyBatisPlus3.5.3.1注意:mybatis-plus-generator版本需与mybatis-plus版本一致最新依赖参考:https://mvnrepository.com/artifact/com.baomid......
  • t04_mybatisplus
    一、快速入门准备数据DROPTABLEIFEXISTSuser;CREATETABLEuser(idBIGINT(20)NOTNULLCOMMENT'主键ID',nameVARCHAR(30)NULLDEFAULTNULLCOMMENT'姓名',ageINT(11)NULLDEFAULTNULLCOMMENT'年龄',emailVARCHAR(50)......
  • mybatisPlus分页查询
    配置类:packagecom.oep.backend.config;importcom.baomidou.mybatisplus.annotation.DbType;importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;importo......
  • MybatisPlus的一些坑
    当使用MybatisPlus更新数据的时候,比如updateById(),update()。如果被更新的对象中的字段是NULL,默认会忽略掉这个为NULL的字段。解决方案有两个:1.使用注解主要是@TableField注解中的属性:updateStrategy属性。但是不推荐这样做,这样做会很危险!!2.使用构造器更新数据建议使用构......
  • spring boot 中使用MybatisPlus的自动填充createTime和updateTime
    首先需要在实体类的字段上加上注解,并且将类型更改为LocalDateTime@TableField(fill=FieldFill.INSERT)@JsonInclude(value=JsonInclude.Include.NON_NULL)@JsonFormat(pattern="yyyy-MM-ddHH:mm:ss")privateLocalDateTimecreateTime;@TableFie......
  • mybatisplus学习笔记01
    常用注解中@TableId注释需要特别注意,不仅要写明值value与表内的属性对应,还要写明是否自增等类型还有@TableField的常见场景需要记忆mp中大部分的配置都是继承的mybatis,所以很类似想要使用mp,基本流程为上图所示......
  • SpringBoot+MybatisPlus+Mysql实现批量插入万级数据多种方式与耗时对比
    场景若依前后端分离版本地搭建开发环境并运行项目的教程:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662若依前后端分离版如何集成的mybatis以及修改集成mybatisplus实现Mybatis增强:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1362030......
  • 若依前后端分离版如何集成的mybatis以及修改集成mybatisplus实现Mybatis增强
    场景若依前后端分离版手把手教你本地搭建环境并运行项目:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662SpringBoot中使用PageHelper插件实现Mybatis分页:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/136189442在上面搭建若依前后端分......
  • MyBatisPlus
     添加依赖<!--MyBatisPlus的依赖--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><!--mysql驱动依赖--......
  • Mybatis Plus java.lang.NoSuchMethodError: com.baomidou.mybatisplus.core.toolkit.
    问题描述在进行SpringBoot整合MybatisPlus时提示10:49:08.390[restartedMain]DEBUGorg.springframework.boot.context.logging.ClasspathLoggingApplicationListener-Applicationfailedtostartwithclasspath:[file:/D:/%e7%99%be%e5%ba%a6%e7%bd%91%e7%9b%98/Vue......