MyBatis-Plus学习记录

1、初探

1.1、简介

官网文档:https://mp.baomidou.com/guide/

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

1.2、SpringBoot快速集成

1.2.1、创建SpringBoot工程

这里怎么创建SpringBoot工程就不做详细的介绍了,创建一个SpringBoot工程有如下方式:

  • 在SpringBoot官网上快速生成:https://start.spring.io/
  • 使用IDEA的Spring Initializr进行创建
  • 使用Maven手动创建添加相关依赖

1.2.2、添加MyBatis-Plus依赖

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

1.2.3、添加MySql即Lombok依赖

既然是使用MyBatis,那么数据库一定是少不了的。然后为了简化我们的代码,这里使用到了Lombok。

如对Lombok不熟的可以顺带了解下Lombok具体使用。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
</dependency>

1.2.4、完整的SpringBoot的pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring-boot</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
                    <mainClass>com.tenghu.sbi.Application</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.2.5、准备一张万能的用户表

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(240) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `address` varchar(2000) DEFAULT NULL COMMENT '家庭住址',
  `phone` varchar(11) DEFAULT NULL COMMENT '联系电话',
  `crt_time` datetime DEFAULT NULL COMMENT '创建时间',
  `upd_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8 COMMENT='用户信息表'

1.2.6、SpringBoot配置

server:
  port: 8088
  servlet:
    context-path: /smp

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: xiaohu

mybatis-plus:
  configuration:
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/*.xml

这里的mybatis-plus.configuration.log-impl配置的是MyBatis日志

1.2.7、编写Entity、Mapper、Service、Controller代码

  • 编写表对应的Entity
@Data
@TableName(value = "user")
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    @TableField(value = "name")
    private String name;
    @TableField(value = "age")
    private Integer age;
    @TableField(value = "address")
    private String address;
    @TableField(value = "phone")
    private String phone;
    @TableField(value = "crt_time",fill = FieldFill.INSERT)
    private Timestamp crtTime;
    @TableField(value = "upd_time",fill = FieldFill.UPDATE)
    private Timestamp updTime;
}

  • 编写Mapper
public interface UserMapper extends BaseMapper<User> {
}

BaseMapper接口中默认支持日常常用的一些对数据的操作方法,查看源码如下:

public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);

    int deleteById(Serializable id);

    int deleteByMap(@Param("cm") Map<String, Object> columnMap);

    int delete(@Param("ew") Wrapper<T> queryWrapper);

    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    int updateById(@Param("et") T entity);

    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

    T selectById(Serializable id);

    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);

    T selectOne(@Param("ew") Wrapper<T> queryWrapper);

    Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);

    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);

    List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);

    <E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);

    <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}

到这里Mapper就完成了,还需要在启动类中添加扫描Mapper接口对应的包,如下:

@SpringBootApplication(scanBasePackages = "com.tenghu.smp")
@MapperScan(basePackages = "com.tenghu.smp.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

其实这里也可以不用配置@MapperScan注解,如果不配置@MapperScan注解,就需要在对应的UserMapper上加@Mapper注解。

  • 编写Service
//接口
public interface UserService {
    /**
     * 获取所有用户
     * @return
     */
    List<User> queryAllUser();

    /**
     * 添加用户
     * @param user
     */
    void addUser(User user);

    /**
     * 修改用户
     * @param user
     */
    void updUser(User user);
}

//实现
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> queryAllUser() {
        return userMapper.selectList(null);
    }

    @Override
    public void addUser(User user) {
        userMapper.insert(user);
    }

    @Override
    public void updUser(User user) {
        Integer userId = Optional.ofNullable(user.getId()).orElseThrow(()->new RuntimeException("用户ID不能为空!"));
        User oldUser = Optional.ofNullable(userMapper.selectById(userId)).orElseThrow(()->new RuntimeException("用户不存在!"));
        BeanUtils.copyProperties(user,oldUser);
        userMapper.updateById(oldUser);
    }
}

这里的更新方法中用到了java8中的Optional类来判断对象是否为空。

  • 编写Controller
@RestController
@RequestMapping(path = "/api/v1/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping(path = "/all")
    public RestResponse queryAllUser(){
        return RestResponse.getOkMessage(userService.queryAllUser());
    }

    @PostMapping(path = "")
    public RestResponse addUser(@RequestBody User user){
        userService.addUser(user);
        return RestResponse.getOkMessage();
    }

    @PutMapping(path = "/{userId}")
    public RestResponse updUser(@PathVariable(name = "userId") Integer userId,
                                @RequestBody User user){
        user.setId(userId);
        userService.updUser(user);
        return RestResponse.getOkMessage();
    }
}

这里封装了个简单的统一出参格式,如下:

@Data
public class RestResponse {
    private String code;
    private String message;
    private Object data;

    private RestResponse(String code,String message,Object data){
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static RestResponse getOkMessage(){
        return new RestResponse("0","success",null);
    }

    public static RestResponse getOkMessage(Object data){
        return new RestResponse("0","success",data);
    }

    public static RestResponse getOkMessage(String message,Object data){
        return new RestResponse("0",message,data);
    }

    public static RestResponse getOkMessage(String message){
        return new RestResponse("0",message,null);
    }

    public static RestResponse getErrMessage(String message){
        return new RestResponse("1",message,null);
    }
}

到这为止,SpringBoot整合MyBatis-Plus就算完成了,对于测试,我们可以直接用Postman来调用写的接口,也可以写测试类。这里我直接使用Postman调用接口,日志打印如下:
(1)添加用户:

==>  Preparing: INSERT INTO user ( name, age, address, phone, crt_time ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 小B(String), 45(Integer), 长隆一路(String), 120(String), null
<==    Updates: 1

(2)查询用户:

==>  Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user
==> Parameters: 
<==    Columns: id, name, age, address, phone, crt_time, upd_time
<==        Row: 51, 小B, 45, 长隆一路, 120, null, null
<==      Total: 1

(3)更新用户:

==>  Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, upd_time=? WHERE id=?
==> Parameters: 小A(String), 45(Integer), 长隆一路(String), 120(String), null, 51(Integer)
<==    Updates: 1

2、扩展功能

在这里一些简单的功能就不做介绍了,下面介绍实际工作中常用的一些扩展功能。

2.1、条件构造器

官网文档:https://mp.baomidou.com/guide/wrapper.html

在实际工作中肯定会遇到多个条件查询数据,如果没有条件构造器,那么就只能在xml文件中写sql语句,既然用了MyBatis-Plus,那么就能少写SQL就尽量的少写。MyBatis-Plus的条件构造器分为两种:查询与更新两种。

2.1.1、查询构造器

查询构造器有如下几种:

  • QueryWrapper
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("address","天府");
queryWrapper.gt("age",15);
List<User> userList = userMapper.selectList(queryWrapper);
  • QueryChainWrapper:
List<User> userList = new QueryChainWrapper<>(userMapper)
                .like("address","天府")
                .gt("age",15)
                .list();
  • LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.like(User::getAddress,"天府");
lambdaQueryWrapper.gt(User::getAge,15);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
  • LambdaQueryChainWrapper
List<User> userList = new LambdaQueryChainWrapper<>(userMapper)
                .like(User::getAddress,"天府")
                .gt(User::getAge,15).list();

从写法上来看,个人更偏向使用Lambda表达式,因为如果后面字段变了,使用到对应的字段在编译的时候就会报错;如果使用简单版的,传入字符串的字段,实体类的字段改了,相应用到改的字段就不容易发现,最终会导致上线后出现异常。
下面是以上几种查询的日志:

==>  Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user WHERE (address LIKE ? AND age > ?)
==> Parameters: %天府%(String), 15(Integer)
<==    Columns: id, name, age, address, phone, crt_time, upd_time
<==        Row: 57, 小AA, 25, 天府三路, 110, 2021-03-27 20:55:22, null
<==        Row: 58, 小AB, 25, 天府四路, 110, 2021-03-27 20:55:36, null
<==      Total: 2

2.1.2 、修改构造器

修改构造器与查询构造器一样,也是有4种。

  • UpdateWrapper
User user = new User();
user.setName("小D");
user.setAge(25);
//修改的查询条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("name","小AA");
userMapper.update(user,updateWrapper);
  • UpdateChainWrapper
User user = new User();
user.setName("小CD");
user.setAge(25);
UpdateChainWrapper updateChainWrapper = new UpdateChainWrapper(userMapper);
updateChainWrapper.eq("name","小D");
updateChainWrapper.update(user);
  • LambdaUpdateWrapper
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper();
        lambdaUpdateWrapper.eq(User::getName,"小CD");
        userMapper.update(user,lambdaUpdateWrapper);
  • LambdaUpdateChainWrapper
boolean result = new LambdaUpdateChainWrapper<>(userMapper)
                .eq(User::getName,"小AB")
                .update(user);

以上打印出的日志如下:

==>  Preparing: UPDATE user SET name=?, age=?, upd_time=? WHERE (name = ?)
==> Parameters: 小AB(String), 25(Integer), 2021-03-28 00:44:15.476(Timestamp), 小AB(String)
<==    Updates: 0

2.2 分页

在实际开发中,分页功能那是避免不了的,使用MyBatis-Plus的分页插件如下:

  • 首先需要定义分页使用到的Bean
@Configuration
public class MyBatisConfiguration {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
}
  • 在Service中使用如下
Page<User> page = new Page<>();
userMapper.selectPage(page,null);

可以对Page的参数进行指定,在实际中我们有可能用不完Page中的所有信息,我们可以自己封装一层自己想用的信息,然后从Page里面提取出来给前端。输出日志如下:

==>  Preparing: SELECT COUNT(*) FROM user
==> Parameters: 
<==    Columns: COUNT(*)
<==        Row: 4
<==      Total: 1
==>  Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user LIMIT ?
==> Parameters: 10(Long)
<==    Columns: id, name, age, address, phone, crt_time, upd_time
<==        Row: 55, 小C, 15, 天府一路, 110, 2021-03-27 20:54:56, null
<==        Row: 56, 小A, 15, 天府二路, 110, 2021-03-27 20:55:07, null
<==        Row: 57, 小FD, 24, 天府三路, 110, 2021-03-27 20:55:22, 2021-03-27 16:41:39
<==        Row: 58, 小FD, 24, 天府四路, 110, 2021-03-27 20:55:36, null
<==      Total: 4

2.3 自动填充功能

自动填充功能一般应用在一些默认字段或新增/修改时自动写入,比如创建时间、更新时间等字段。实现步骤如下:

  • 在需要自动填充的字段加上填充类型
@TableField(value = "crt_time",fill = FieldFill.INSERT)
private Timestamp crtTime;
@TableField(value = "upd_time",fill = FieldFill.UPDATE)
private Timestamp updTime;

这里对创建时间插入时自动填充,更新时间字段更新时自动填充。

  • 使用一个组件处理自动填充
@Component
@Slf4j
public class MetaObjectComponent implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ...............");
        this.strictInsertFill(metaObject,"crtTime",Timestamp.class,new Timestamp(System.currentTimeMillis()));
        log.info("end insert fill .................");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ...............");
        this.strictUpdateFill(metaObject,"updTime",Timestamp.class,new Timestamp(System.currentTimeMillis()));
        log.info("end update fill .................");
    }
}

注意:自动填充的值类型必须与实体类中的字段类型一致,不然就会插入Null。

插入一条数据测试结果如下:
入参:

{"address":"天府四路","age":25,"name":"小ABC","phone":"110"}

入参没有对crtTime赋值,后端service里面也没对crtTime赋值,从日志可以看出,自动填充功能已经生效。

==>  Preparing: INSERT INTO user ( name, age, address, phone, crt_time ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 小ABC(String), 25(Integer), 天府四路(String), 110(String), 2021-03-28 01:00:20.832(Timestamp)
<==    Updates: 1

2.4 乐观锁

官网文档:https://mp.baomidou.com/guide/wrapper.html

实现步骤如下:

  • 添加OptimisticLockerInterceptor
@Bean
public OptimisticLockerInterceptor optimisticLockerInnerInterceptor(){
    return new OptimisticLockerInterceptor();
}

注意:官网文档这里是提示用OptimisticLockerInnerInterceptor,但是经过测试会报异常,因此还是用OptimisticLockerInterceptor类,虽然提示已失效,但测试结果version增加了。

  • 在实体乐观锁版本增加@version注解
@Version
@TableField(value = "version")
private Integer version;
  • 测试
userMapper.updateById(oldUser);

oldUser.setVersion(user.getVersion());
oldUser.setName("李四");
userMapper.updateById(oldUser);

上面代码,更新一次后将旧版本再设置回去并将姓名改了,日志如下:

==>  Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, version=?, upd_time=? WHERE id=? AND version=?
==> Parameters: 小CB(String), 25(Integer), 天府四路(String), 110(String), 12(Integer), 2021-03-28 01:35:11.132(Timestamp), 55(Integer), 11(Integer)
<==    Updates: 1
==>  Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, version=?, upd_time=? WHERE id=? AND version=?
==> Parameters: 李四(String), 25(Integer), 天府四路(String), 110(String), 12(Integer), 2021-03-28 01:35:11.132(Timestamp), 55(Integer), 11(Integer)
<==    Updates: 0

从日志可以看出第二次并没有修改成功,因为第一次修改版本已经升级,第二次修改的版本号还是旧版本,因此对旧版本号的数据修改失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小老虎Love

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值