Mybatis Plus 系列4 —— Mapper CRUD之更新


前几章主要是讲的查询相关的功能,这次我们来说说更新相关的功能!

一. 前置准备

先来看看我们用来演示的项目的结构:

.
├── HELP.md
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── cn
    │   │       └── justmr
    │   │           └── mybatisplus
    │   │               └── demo
    │   │                   ├── DemoApplication.java
    │   │                   ├── config
    │   │                   │   └── MybatisPlusConfig.java // 分页配置
    │   │                   ├── entity
    │   │                   │   ├── BaseEntity.java
    │   │                   │   └── User.java
    │   │                   ├── handler
    │   │                   │   ├── MyMetaObjectHandler.java // 自动填充 create_time 和 update_time
    │   │                   └── mapper
    │   │                       └── UserMapper.java
    │   └── resources
    │       ├── application.yml
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── cn
                └── justmr
                    └── mybatisplus
                        └── demo
                            └── demo
                                ├── DemoApplicationTests.java
                                ├── UserTests.java

1. build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'cn.justmr.mybatis-plus.demo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    // 为了加快速度,配置阿里镜像仓库
    maven { url "http://maven.aliyun.com/nexus/content/groups/public" }
    mavenCentral()
}

ext {
    set('mybaitsPlusVersion', "3.4.2")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    // 引入mybatis plus
    compile "com.baomidou:mybatis-plus-boot-starter:${mybaitsPlusVersion}"
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}


test {
    useJUnitPlatform()
}

2. application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: test
    password: 123456

mybatis-plus:
  configuration:
    # 开启自动驼峰命名规则(camel case)映射,默认就是true
    map-underscore-to-camel-case: true
    # 输出日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      insert-strategy: not_null
      update-strategy: not_null # 默认全局更新策略

3. 测试数据

测试数据

二. 更新

首先还是先看一下UserMapper实现的BaseMapper的接口:

1. 更新策略

我们先看一下默认的全局更新策略:

# 在application.yml中配置了默认的更新策略
mybatis-plus:
  global-config:
    db-config:
      insert-strategy: not_null # 默认全局插入策略
      update-strategy: not_null # 默认全局更新策略

修改全部插入/更新策略,可以参考Mybatis Plus 常用注解.

这个配置说明,在更新的时候,如果T的属性值为null则忽略不更新,演示一下:

@Test
@DisplayName("更新测试1")
public void test1() {
  User user = new User();
  user.setName("shiv");
  user.setAge(28);
  user.setEmail("22@qq.com");
  user.setSex("MAIL");

  // 插入一条数据
  userMapper.insert(user);

  // 更新邮箱
  user.setEmail("22@shiv.com");
  userMapper.updateById(user);

  // 查询更新后结果
  User data = userMapper.selectById(user.getId());
  Assertions.assertEquals(data.getEmail(), "22@shiv.com");
}

结果:

==>  Preparing: INSERT INTO user ( name, age, email, sex, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: shiv(String), 28(Integer), 22@qq.com(String), MAIL(String), 2021-02-13T22:56:49.267(LocalDateTime), 2021-02-13T22:56:49.271(LocalDateTime)
<==    Updates: 1
...
==>  Preparing: UPDATE user SET name=?, age=?, email=?, sex=?, create_time=?, update_time=? WHERE id=?
==> Parameters: shiv(String), 28(Integer), 22@shiv.com(String), MAIL(String), 2021-02-13T22:56:49.267(LocalDateTime), 2021-02-13T22:56:49.271(LocalDateTime), 7(Long)
<==    Updates: 1
...
==>  Preparing: SELECT id,name,age,email,sex,is_delete,create_time,update_time FROM user WHERE id=?
==> Parameters: 7(Long)
<==    Columns: id, name, age, email, sex, is_delete, create_time, update_time
<==        Row: 7, shiv, 28, 22@shiv.com, MAIL, 0, 2021-02-13 22:56:49, 2021-02-13 22:56:49
<==      Total: 1

思考:

其实上面的更新,我只想更新一下email信息,并不想更行其它字段,但是由于使用的实体和插入的是同一个,导致其他字段也加入到了最终的sql中,从打印的SQL日志上看,完全看不出我是要更新哪个字段!我是比较主张不要为了方便把不需要更新的字段也一并更新掉,增加了很多干扰信息!

所以上面的测试稍微改一下:

@Test
@DisplayName("更新测试2")
public void test2() {
  User user = new User();
  user.setName("shiv");
  user.setAge(28);
  user.setEmail("22@qq.com");
  user.setSex("MAIL");

  // 插入一条数据
  userMapper.insert(user);

  // 更新邮箱, 其它字段不设置默认为null,不会更新
  User u = new User();
  u.setId(user.getId());
  u.setEmail("22@shiv.com");
  userMapper.updateById(u);

  // 查询更新后结果
  User data = userMapper.selectById(user.getId());
  Assertions.assertEquals(data.getEmail(), "22@shiv.com");
}

再对比下打印日志:

==>  Preparing: INSERT INTO user ( name, age, email, sex, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: shiv(String), 28(Integer), 22@qq.com(String), MAIL(String), 2021-02-13T23:07:44.490(LocalDateTime), 2021-02-13T23:07:44.494(LocalDateTime)
<==    Updates: 1
...
==>  Preparing: UPDATE user SET email=?, update_time=? WHERE id=?
==> Parameters: 22@shiv.com(String), 2021-02-13T23:07:45.378(LocalDateTime), 8(Long)
<==    Updates: 1
...
==>  Preparing: SELECT id,name,age,email,sex,is_delete,create_time,update_time FROM user WHERE id=?
==> Parameters: 8(Long)
<==    Columns: id, name, age, email, sex, is_delete, create_time, update_time
<==        Row: 8, shiv, 28, 22@shiv.com, MAIL, 0, 2021-02-13 23:07:44, 2021-02-13 23:07:45
<==      Total: 1

可以看到更新邮箱的时候,只给实体u设置了主键和email字段,其它的字段默认为nullset时就忽略了那些为null的字段。

然后我们可以更新默认的更新策略,如改为下面的:

# 在application.yml中配置了默认的更新策略
mybatis-plus:
  global-config:
    db-config:
      update-strategy: IGNORED # 忽略判断

重新跑一下单元测试,结果失败,因为数据库有些字段设置了不能为null,我们主要关注一下更新的SQL:

==>  Preparing: UPDATE user SET name=?, age=?, email=?, sex=?, is_delete=?, create_time=?, update_time=? WHERE id=?
==> Parameters: null, null, 22@shiv.com(String), null, null, null, 2021-02-13T23:16:00.285(LocalDateTime), 9(Long)

可以看到为null的字段也进行了更新!

除了修改全局的update-strategy外,我们也可以在实体类对应字段上添加对应注解@TableField(updateStrategy = FieldStrategy.IGNORED),这里就不再演示了!最后将update-strategy配置还原为not_null

2. updateById(T)

根据 ID 修改

使用方式参考上面的示例

3. update(T, Wrapper)

根据 whereEntity 条件,更新记录
Params: entity – 实体对象 (set 条件值,可以为 null)
updateWrapper – 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句

int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);

从注释中可以看到两个参数,entity主要用于生成set条件。updateWrapper则复杂一下,可生成where语句,也可以生成set条件值!我们一起来试一试:

示例1
@Test
@DisplayName("更新测试3")
public void test3() {
  // entity 为 null 的情况
  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .set(User::getEmail, "33@shiv.com")
    .eq(User::getId, 8);

  userMapper.update(null, wrapper);
}

结果:

==>  Preparing: UPDATE user SET email=? WHERE (id = ?)
==> Parameters: 33@shiv.com(String), 8(Integer)
<==    Updates: 1

注意:使用这种方式设置某个字段为null, 不会被忽略的,如下:

@Test
@DisplayName("更新测试4")
public void test4() {
  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .set(User::getEmail, null)
    .eq(User::getId, 8);

  // entity 为 null 的情况
  userMapper.update(null, wrapper);
}

结果:

==>  Preparing: UPDATE user SET email=? WHERE (id = ?)
==> Parameters: null, 8(Integer)

可见 email 依然会被设置为 null.所以在更新策略为not_null的情况下,如果需要将某个字段设置为null,可以采用这种方法(在updateWrapper里设置)。

示例2
@Test
@DisplayName("更新测试5")
public void test5() {
  User u = new User();
  u.setId(8L);
  u.setEmail("44@shiv.com");

  // updateWrapper为空的情况
  userMapper.update(u, null);
}

结果:

==>  Preparing: UPDATE user SET email=?, update_time=?
==> Parameters: 44@shiv.com(String), 2021-02-13T23:42:25.512(LocalDateTime)
<==    Updates: 8

注意:

  1. entity参数里面为null的会字段被忽略(更新策略影响)
  2. entity参数不会在where条件中
  3. 主键不会应用在set条件中
示例3
@Test
@DisplayName("更新测试6")
public void test6() {
  User u = new User();
  u.setEmail("44@shiv.com");

  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .eq(User::getId, 8);

  userMapper.update(u, wrapper);
}

结果:

==>  Preparing: UPDATE user SET email=?, update_time=? WHERE (id = ?)
==> Parameters: 44@shiv.com(String), 2021-02-13T23:50:25.951(LocalDateTime), 8(Integer)
<==    Updates: 1
示例4(避免)

entityupdateWrapper都对相同字段设置,这种情况尽量避免,不是那么好理解

@Test
@DisplayName("更新测试6")
public void test6() {
  // entity和 updateWrapper 都对 email设置值
  User u = new User();
  u.setEmail("44@shiv.com");

  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .set(User::getEmail, "44@shiv.com")
    .eq(User::getId, 8);

 
  userMapper.update(u, wrapper);
}

结果emailset中出现了两次:

==>  Preparing: UPDATE user SET email=?, update_time=?, email=? WHERE (id = ?)
==> Parameters: 44@shiv.com(String), 2021-02-13T23:54:00.213(LocalDateTime), 44@shiv.com(String), 8(Integer)
<==    Updates: 1
示例5

利用 updateWrapper 里的entity生成where条件

@Test
@DisplayName("更新测试6")
public void test6() {
  User entity = new User();
  entity.setId(8L);
  entity.setName("shiv");

  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .set(User::getEmail, "44@shiv.com")
    .setEntity(entity);

  userMapper.update(null, wrapper);
}

结果:

==>  Preparing: UPDATE user SET email=? WHERE id=? AND name=?
==> Parameters: 44@shiv.com(String), 8(Long), shiv(String)
<==    Updates: 1

注意:
这种方式更新,不会自动更新update_time的值,尽管我们设置了update_time的自动填充功能!填充只能填充到entity里

示例6(避免)
@Test
@DisplayName("更新测试7")
public void test7() {
  User entity = new User();
  entity.setId(8L); // id
  entity.setName("shiv");

  LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate()
    .set(User::getEmail, "44@shiv.com")
    .eq(User::getId, 8L) // id 
    .setEntity(entity);

  userMapper.update(null, wrapper);
}

结果where条件中,id出现了两次:

==>  Preparing: UPDATE user SET email=? WHERE id=? AND name=? AND (id = ?)
==> Parameters: 44@shiv.com(String), 8(Long), shiv(String), 8(Long)
<==    Updates: 1

4. 链式更新(推荐)

@Test
@DisplayName("更新测试8")
public void test8() {
  ChainWrappers.lambdaUpdateChain(userMapper)
    .set(User::getEmail, "44@shiv.com")
    .eq(User::getId, 8L)
    .update();
}

结果:

==>  Preparing: UPDATE user SET email=? WHERE (id = ?)
==> Parameters: 44@shiv.com(String), 8(Long)
<==    Updates: 1

三. 思考

对于一些更新场景,就针对根据主键更新(updateById(T))这个方法来讲,有时我们只想更新我们设置的不为null的字段,有时我们又想传什么值就更新成什么值,包括null(表单清空情况)。对于这些情况,早期mybatis plus是通过两个api来做的updateupdateSelective,其它的框架也是类似的实现,比如 tk.mapper。但是自v2.0.0[2016.12.11]版本版本开始,mybatis plus合并了所有Selective通用方法(例如:去除之前的insert方法并把之前的insetSelective改名为insert),导致我们没法直接使用这两个方法来处理对应的场景了!其实对于传什么值就更新成什么值(包括null)的场景,利用早期[v1.4.9]的update方法,则会将涉及到的全部字段更新,不管是不是这次需要更新的。而且通常还需要提前将更新前的数据查询出来,设置值,再去更新,这样做虽然方便,但是并不是很好!对于更新字段我还是建议更新哪个字段就只set哪个字段,不要不管三七二十一的全部set一遍。

mybatis plus中使用更新的建议:

  1. 采用默认的全局插入,更新策略not_null
  2. 如果有某些字段需要更新为null,建议采用上面链式更新(推荐)中的用法,手动调用set方法!

文章作者: shiv
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shiv !
评论
  目录