Mybatis-Plus入门(1)

单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.
官方网站如下:
简介 | MyBatis-Plus
当然,MybatisPlus不仅仅可以简化单表操作,而且还对Mybatis的功能有很多的增强。可以让我们的开发更加的简单,高效。

1.CRUD快速入门

一、Spring Boot整合Mybatis Plus

第一步:通过maven坐标引入依赖


<dependency>
  <groupId>com.baomidougroupId>
  <artifactId>mybatis-plus-boot-starterartifactId>
  <version>3.5.3.1version>
dependency>

<dependency>
  <groupId>com.mysqlgroupId>
  <artifactId>mysql-connector-jartifactId>
  <scope>runtimescope>
dependency>

<dependency>
  <groupId>org.projectlombokgroupId>
  <artifactId>lombokartifactId>
  <optional>trueoptional>
dependency>

第二步:application配置数据源及日志输出级别

# 配置数据源
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
# 配置日志
logging:
  level:
    com.itheima: debug
  pattern:
    dateformat: HH:mm:ss

第三步:配置Mybatis的Mapper类文件的包扫描路径

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

二、编码构建实体和Mapper

编写实体类User.java

@Data
@TableName(value = "user", autoResultMap = true)
public class User {

    /**
     * 用户id
     */
    @TableId
    private Long id;

    /**
     * 用户名
     */
    @TableField("`username`")
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 注册手机号
     */
    private String phone;

    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;

    /**
     * 使用状态(1正常 2冻结)
     */
    private UserStatus status;

    /**
     * 账户余额
     */
    private Integer balance;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private LocalDateTime updateTime;
}

编写Mapper类UserMapper.java

@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {

}

2.常见注解

在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了:
MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?

大家回忆一下,UserMapper在继承BaseMapper的时候指定了一个泛型:
Mybatis-Plus入门(1)_第1张图片
泛型中的User就是与数据库对应的PO.

MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名
  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus会把名为id的字段作为主键

但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。

2.1.@TableName

说明:

  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类

示例:

@TableName("user")
public class User {
    private Long id;
    private String name;
}

TableName注解除了指定表名以外,还可以指定很多其它属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
schema String “” schema
keepGlobalPrefix boolean false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMap String “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMap boolean false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludeProperty String[] {} 需要排除的属性名 @since 3.3.1

2.2@TableId

说明:

  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段

示例:

@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
}

TableId注解支持两个属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
type Enum IdType.NONE 指定主键类型

IdType支持的类型有:

描述
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策略

2.3.@TableField

说明:

描述:普通字段注解

示例:

@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    @TableField("isMarried")
    private Boolean isMarried;
    @TableField("`concat`")
    private String concat;
}

一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致
  • 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加````转义

支持的其它属性如下:

属性 类型 必填 默认值 描述
value String “” 数据库字段名
exist boolean true 是否为数据库表字段
condition String “” 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window)
update String “” 字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategy Enum FieldStrategy.DEFAULT 举例:NOT_NULL
insert into table_a(column) values (#{columnProperty})
updateStrategy Enum FieldStrategy.DEFAULT 举例:IGNORED
update table_a set column=#{columnProperty}
whereStrategy Enum FieldStrategy.DEFAULT 举例:NOT_EMPTY
where column=#{columnProperty}
fill Enum FieldFill.DEFAULT 字段自动填充策略
select boolean true 是否进行 select 查询
keepGlobalFormat boolean false 是否保持使用全局的 format 进行处理
jdbcType JdbcType JdbcType.UNDEFINED JDBC 类型 (该默认值不代表会按照该值生效)
typeHandler TypeHander
类型处理器 (该默认值不代表会按照该值生效)
numericScale String “” 指定小数点后保留的位数

3.常见配置

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

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

  • 实体类的别名扫描包
  • 全局id类型
mybatis-plus:
  type-aliases-package: com.itheima.mp.domain.po
  global-config:
    db-config:
      id-type: auto # 全局id类型为自增长

需要注意的是,MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置:

mybatis-plus:
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

可以看到默认值是classpath*:/mapper/**/*.xml,也就是说我们只要把mapper.xml文件放置这个目录下就一定会被加载。

例如,我们新建一个UserMapper.xml文件:
Mybatis-Plus入门(1)_第2张图片
然后在其中定义一个方法:


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">

    <select id="queryById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    select>
mapper>

然后在测试类UserMapperTest中测试该方法:

@Test
void testQuery() {
    User user = userMapper.queryById(1L);
    System.out.println("user = " + user);
}

4.核心功能

刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。

4.1.条件构造器

除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。
Mybatis-Plus入门(1)_第3张图片
参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Mybatis-Plus入门(1)_第4张图片

Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:
Mybatis-Plus入门(1)_第5张图片
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:
Mybatis-Plus入门(1)_第6张图片
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
Mybatis-Plus入门(1)_第7张图片

接下来,我们就来看看如何利用Wrapper实现复杂查询。

4.1.1.QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子:
查询:查询出名字中带o的,存款大于等于1000元的人。代码如下:

@Test
void testQueryWrapper() {
    // 1.构建查询条件 where name like "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .select("id", "username", "info", "balance")
            .like("username", "o")
            .ge("balance", 1000);
    // 2.查询数据
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

更新:更新用户名为jack的用户的余额为2000,代码如下:

@Test
void testUpdateByQueryWrapper() {
    // 1.构建查询条件 where name = "Jack"
    QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
    // 2.更新数据,user中非null字段都会作为set语句
    User user = new User();
    user.setBalance(2000);
    userMapper.update(user, wrapper);
}

4.1.2.UpdateWrapper

基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id为1,2,4的用户的余额,扣200,对于的SQL应该是:

UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:

@Test
void testUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200") // SET balance = balance - 200
            .in("id", ids); // WHERE id in (1, 2, 4)
	// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
    // 而是基于UpdateWrapper中的setSQL来更新
    userMapper.update(null, wrapper);
}

4.1.3.LambdaQueryWrapper

无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的gettter方法结合反射技术。因此我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用Lambda表达式。
因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应QueryWrapper和UpdateWrapper

其使用方式如下:

@Test
void testLambdaQueryWrapper() {
    // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.lambda()
            .select(User::getId, User::getUsername, User::getInfo, User::getBalance)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);
    // 2.查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

更多构造器使用方法总结
Mybatis-Plus入门(1)_第8张图片

4.2.自定义SQL

在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:
Mybatis-Plus入门(1)_第9张图片
这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。
这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。

所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL

4.2.1.基本用法

以当前案例来说,我们可以这样写:

@Test
void testCustomWrapper() {
    // 1.准备自定义查询条件
    List<Long> ids = List.of(1L, 2L, 4L);
    QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);

    // 2.调用mapper的自定义方法,直接传递Wrapper
    userMapper.deductBalanceByIds(200, wrapper);
}

然后在UserMapper中自定义SQL:

public interface UserMapper extends BaseMapper<User> {
    @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
    void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}

这样就省去了编写复杂查询条件的烦恼了。

4.2.2.多表关联

理论上来将MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户
要是自己基于mybatis实现SQL,大概是这样的:

  <select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
      SELECT *
      FROM user u
      INNER JOIN address a ON u.id = a.user_id
      WHERE u.id
      <foreach collection="ids" separator="," item="id" open="IN (" close=")">
          #{id}
      foreach>
      AND a.city = #{city}
  select>

可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。
查询条件这样来构建:

@Test
void testCustomJoinWrapper() {
    // 1.准备自定义查询条件
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .in("u.id", List.of(1L, 2L, 4L))
            .eq("a.city", "北京");

    // 2.调用mapper的自定义方法
    List<User> users = userMapper.queryUserByWrapper(wrapper);

    users.forEach(System.out::println);
}

然后在UserMapper中自定义方法:

@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);

当然,也可以在UserMapper.xml中写SQL:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
    SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
select>

4.3.Service接口

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

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

4.3.1.CRUD

我们先俩看下基本的CRUD接口。
新增
Mybatis-Plus入门(1)_第10张图片

  • save是新增单个元素
  • saveBatch是批量新增
  • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch是批量的新增或修改

删除:
Mybatis-Plus入门(1)_第11张图片

  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper):根据Wrapper条件删除
  • ~~removeBatchByIds~~:暂不支持

修改:
Mybatis-Plus入门(1)_第12张图片

  • updateById:根据id修改
  • update(Wrapper):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

Get:
Mybatis-Plus入门(1)_第13张图片

  • getById:根据id查询1条数据
  • getOne(Wrapper):根据Wrapper查询1条数据
  • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

List:
Mybatis-Plus入门(1)_第14张图片

  • listByIds:根据id批量查询
  • list(Wrapper):根据Wrapper条件查询多条数据
  • list():查询所有

Count
Mybatis-Plus入门(1)_第15张图片

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

getBaseMapper
当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
Mybatis-Plus入门(1)_第16张图片

4.3.2.基本用法

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。

首先,定义UserService,继承IService

package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface UserService extends IService<User> {
    // 拓展自定义方法
}

然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService

package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.UserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

项目结构如下:
Mybatis-Plus入门(1)_第17张图片

最后,编写一个测试类,测试一下:

package com.itheima.mp.service;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    void testService() {
        List<User> list = userService.list();
        list.forEach(System.out::println);
    }
}

4.3.3.Lambda

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

基于Lambda查询:

@Test
void testLambdaQuery() {
    // 1.查询1个
    User rose = userService.lambdaQuery()
            .eq(User::getUsername, "Rose")
            .one(); // .one()查询1个
    System.out.println("rose = " + rose);

    // 2.查询多个
    List<User> users = userService.lambdaQuery()
            .like(User::getUsername, "o")
            .list(); // .list()查询集合
    users.forEach(System.out::println);

    // 3.count统计
    Long count = userService.lambdaQuery()
            .like(User::getUsername, "o")
            .count(); // .count()则计数
    System.out.println("count = " + count);
}

可以发现lambdaQuery方法中除了可以构建条件,而且根据链式编程的最后一个方法来判断最终的返回结果,可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

lambdaQuery还支持动态条件查询。比如下面这个需求:

定义一个方法,接收参数为username、status、minBalance、maxBalance,参数可以为空。

  • 如果username参数不为空,则采用模糊查询;
  • 如果status参数不为空,则采用精确匹配;
  • 如果minBalance参数不为空,则余额必须大于minBalance
  • 如果maxBalance参数不为空,则余额必须小于maxBalance

这个需求就是典型的动态查询,在业务开发中经常碰到,实现如下:

@Test
void testQueryUser() {
    List<User> users = queryUser("o", 1, null, null);
    users.forEach(System.out::println);
}

public List<User> queryUser(String username, Integer status, Integer minBalance, Integer maxBalance) {
    return userService.lambdaQuery()
            .like(username != null , User::getUsername, username)
            .eq(status != null, User::getStatus, status)
            .ge(minBalance != null, User::getBalance, minBalance)
            .le(maxBalance != null, User::getBalance, maxBalance)
            .list();
}

基于Lambda更新:

@Test
void testLambdaUpdate() {
    userService.lambdaUpdate()
            .set(User::getBalance, 800) // set balance = 800
            .eq(User::getUsername, "Jack") // where username = "Jack"
            .update(); // 执行Update
}

lambdaUpdate()方法后基于链式编程,可以添加set条件和where条件。但最后一定要跟上update(),否则语句不会执行。

lambdaUpdate()同样支持动态条件,例如下面的需求:

基于IService中的lambdaUpdate()方法实现一个更新方法,满足下列需求:

  • 参数为balance、id、username
  • id或username至少一个不为空,根据id或username精确匹配用户
  • 将匹配到的用户余额修改为balance
  • 如果balance为0,则将用户status修改为冻结状态

实现如下:

@Test
void testUpdateBalance() {
    updateBalance(0L, 1L, null);
}

public void updateBalance(Long balance, Long id, String username){
    userService.lambdaUpdate()
            .set(User::getBalance, balance)
            .set(balance == 0, User::getStatus, 2)
            .eq(id != null, User::getId, id)
            .eq(username != null, User::getId, username)
            .update();
}

4.4.静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:
Mybatis-Plus入门(1)_第18张图片

示例:

@Test
void testDbGet() {
    User user = Db.getById(1L, User.class);
    System.out.println(user);
}

@Test
void testDbList() {
    // 利用Db实现复杂条件查询
    List<User> list = Db.lambdaQuery(User.class)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000)
            .list();
    list.forEach(System.out::println);
}

@Test
void testDbUpdate() {
    Db.lambdaUpdate(User.class)
            .set(User::getBalance, 2000)
            .eq(User::getUsername, "Rose");
}

你可能感兴趣的:(后端,mybatis,java,spring,boot)