SpringBoot集成MyBatis通用Mapper4

@TOC

1、为什么使用通用Mapper

我们知道Mybatis是一个完全与数据库打交道的工具,最初我们对于数据库的操作,即使是使用了Mybatis之后,大部分也都是手动编辑xml配置文件,其实这也是很消耗时间的,后来就出现了MyBatis代码生成器(简称MBG),可以根据数据库链接自动逆向生成实体类Mapper接口XML配置文件 ,但是在后续使用MBG的过程中,如果数据库字段比较多,且变化比较频繁的话,就需要反复重新生成代码,并且由于MBG会覆盖生成代码和追加生成XML配置文件,导致每次重新生成的配置文件都要进行大量的比对修改,这就很烦人了,不是我们最初想要的结果。

其实在我们工作当中大多数情况还有一个问题,就是我们实际操作中对于单表的操作,仅仅只是增删改查,根本没有必要生成一大堆sql,这样的话会使xml文件产生大量冗余,内容多,看着不舒服,运行起来也消耗时间内存。

MBG中其实也定义了很多常用的单标操作方法,为了解决上述的问题,也为了兼容MBG的方法避免项目重构频繁,在MBG的基础上结合了部分JPA的部分注解就产生了通用Mapper,通用Mapper可以很简单的获取基础的单表操作方法,也可以很方便的扩展通用方法。

作用及优点:

  • 极大的提升开发效率,减少工作量。
  • 可以随意按照自己的需要选择通用方法。
  • 很方便的开发自己的通用方法。
  • 极其方便的使用MyBatis单表的正删改查。
  • 支持单表操作,不支持通用的多表联合查询。
  • 强调:

    实体类中不是数据库中的字段需要添加@Transient注解(排除映射)。

2、Spring Boot 集成

Spring Boot 在微服务领域中已经成为主流。

这里简单介绍通用 Mapper 如何同 Spring Boot 进行集成。

为了能适应各种情况的用法,这里也提供了多种集成方式,基本上分为两大类。

  • 基于 starter 的自动配置
  • 基于 @MapperScan 注解的手工配置
在 starter 的逻辑中,如果你没有使用 @MapperScan 注解,你就需要在你的接口上增加 @Mapper 注解,否则 MyBatis 无法判断扫描哪些接口。

通用Mapper需要MyBatis的依赖!!!

2.1、引入相关依赖

                <!--mybatis集成springboot-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!--通用mapper集成springboot-->
        <!-- https://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>

mapper最新版本号如上所示,你也可以从下面地址查看:

http://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter

注意:引入该 starter 时,和 MyBatis 官方的 starter 没有冲突,但是官方的自动配置不会生效!

如果你需要对特定的通用 Mapper 进行配置,你可以在 Spring Boot 的配置文件中配置 mapper. 前缀的配置。

例如在 yml 格式中配置:

mapper:
  mappers:
    - com.wbs.dao.Mapper1  #接口1
    - com.wbs.dao.Mapper2  #接口2 
  notEmpty: true

在 properties 配置中:

mapper.mappers=com.wbs.dao.Mapper1,com.wbs.dao.Mapper2
mapper.notEmpty=true

由于 Spring Boot 支持 Relax 方式的参数,因此你在配置 notEmpty 时更多的是用 not-empty,也只有在 Spring Boot 中使用的时候参数名不必和配置中的完全一致。

2.2、@MapperScan 注解配置

你可以给带有 @Configuration 的类配置该注解,或者直接配置到 Spring Boot 的启动类上,如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.wbs.dao")
public class GeneralMapperApplication {
    public static void main(String[] args) {
        SpringApplication.run(GeneralMapperApplication.class, args);
    }

注意:这里使用的 tk.mybatis.spring.annotation.MapperScan!

你可以直接在 Spring Boot 的配置文件中直接配置通用 Mapper,还可以使用注解中提供的两个属性进行配置:

/**
 * 通用 Mapper 的配置,一行一个配置
 *
 * @return
 */
String[] properties() default {};

/**
 * 还可以直接配置一个 MapperHelper bean
 *
 * @return
 */
String mapperHelperRef() default "";
注意:这两个属性配置方式的优先级更高,所以建议在 Spring Boot 中通过配置文件(或 Environment)配置。

3、对象关系映射

3.1、简单示例

示例针对 MySql 数据库(数据库对主键影响较大,和 insert 关系密切)。

数据库有如下表:

CREATE TABLE `user_info` (  
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', 
  `name` varchar(255) DEFAULT NULL COMMENT '姓名', 
  `des` varchar(255) DEFAULT NULL COMMENT '描述', 
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4

对应的 Java 实体类型如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user_info")
public class UserInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键id
     * @mbg.generated Mon Dec 13 15:55:47 CST 2021
     */
    @Id
    @KeySql(useGeneratedKeys = true)
    private Long id;

    /**
     * 姓名
     * @mbg.generated Mon Dec 13 15:55:47 CST 2021
     */
    private String name;

    /**
     * 描述
     * @mbg.generated Mon Dec 13 15:55:47 CST 2021
     */
    private String des;
}

最简单的情况下,只需要一个 @Id 注解标记字段为主键即可。数据库中的字段名和实体类中的字段名是完全相同的,这中情况下实体和表可以直接映射,也就是不需要@Column注解了。

提醒:如果实体类中没有一个标记 @Id 的字段,当你使用带有 ByPrimaryKey(根据主键查询) 的方法时,所有的字段都会作为主键来联合使用,也就会出现类似 where id = ? and name = ? and des= ? 的情况,这种情况效率是极低的。
  • 后续会介绍代码生成器,可以自动生成上面的实体和下面的接口代码!

通用 Mapper 提供了大量的通用接口,这里以最常用的 Mapper 接口为例

该实体类对应的数据库操作接口如下:

import tk.mybatis.mapper.common.Mapper;

public interface UserInfoMapper extends Mapper<UserInfo> {

}

在SpringBoot中只需在启动类上配置注解@MapperScan("接口路径"), 能注册并且被扫描到,该接口提供的方法就都可以使用。

一旦继承了Mapper<T> 接口, 所继承的Mapper接口就拥有了以下通用的方法:

//根据实体类不为null的字段进行查询,条件全部使用=号and条件
List<T> select(T record);

//根据实体类不为null的字段查询总数,条件全部使用=号and条件
int selectCount(T record);

//根据主键进行查询,必须保证结果唯一
//单个字段做主键时,可以直接写主键的值
//联合主键时,key可以是实体类,也可以是Map
T selectByPrimaryKey(Object key);

//插入一条数据
//支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写)
//优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长
int insert(T record);

//插入一条数据,只插入不为null的字段,不会影响有默认值的字段
//支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写)
//优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长
int insertSelective(T record);

//根据实体类中字段不为null的条件进行删除,条件全部使用=号and条件
int delete(T key);

//通过主键进行删除,这里最多只会删除一条数据
//单个字段做主键时,可以直接写主键的值
//联合主键时,key可以是实体类,也可以是Map
int deleteByPrimaryKey(Object key);

//根据主键进行更新,这里最多只会更新一条数据
//参数为实体类
int updateByPrimaryKey(T record);

//根据主键进行更新
//只会更新不是null的数据
int updateByPrimaryKeySelective(T record);

以上是常见的增删改查单表操作,还有很多类似的方法就不一一列举了!

3.2、测试

从 Spring容器中获取到该接口的Bean之后就可以直接使用:

        @Autowired
    UserInfoMapper userInfoMapper;

    /**
     * 使用通用mapper中的selectAll方法
     */
    @Test
    void test1(){
        List<UserInfo> userInfos = userInfoMapper.selectAll();
        for (UserInfo userInfo : userInfos) {
            System.out.println(userInfo.toString());
        }
    }

    /**
     * 根据实体类不为null的字段查询总数,条件全部使用=号and条件
     */
    @Test
    void test3(){
        int count = userInfoMapper.selectCount(new UserInfo( 1L,"张三","法外狂徒"));
        System.out.println("人数为:" + count);
    }

    /**
     * 根据主键进行查询,必须保证结果唯一
     * 单个字段做主键时,可以直接写主键的值
     * 联合主键时,key可以是实体类,也可以是Map
     */
    @Test
    void test4(){
        UserInfo userInfo1 = new UserInfo();
        userInfo1.setId(1L);
        UserInfo userInfo = userInfoMapper.selectByPrimaryKey(userInfo1);
        System.out.println("主键为1的货是:" + userInfo);
    }

    /**
     * 插入一条数据
     * 支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写)
     * 优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长
     */
    @Test
    void test5(){
        UserInfo userInfo = new UserInfo();
        userInfo.setName("马六");
        userInfo.setDes("吃瓜群众");
        int result = userInfoMapper.insert(userInfo);
        if (result > 0){
            System.out.println(userInfo + ":该条数据插入成功!");
        }
    }

    /**
     * 插入一条数据,只插入不为null的字段,不会影响有默认值的字段
     * 支持Oracle序列,UUID,类似Mysql的INDENTITY自动增长(自动回写)
     * 优先使用传入的参数值,参数值空时,才会使用序列、UUID,自动增长
     */
    @Test
    void test6(){
        UserInfo userInfo = new UserInfo();
        userInfo.setName("啥七");
        //userInfo.setDes("啥也不是");
        int result = userInfoMapper.insertSelective(userInfo);
        if (result > 0){
            System.out.println(userInfo + ":该条数据插入成功!");
        }
    }

    /**
     * 根据实体类中字段不为null的条件进行删除,条件全部使用=号and条件
     */
    @Test
    void test7(){
        UserInfo userInfo = new UserInfo();
        userInfo.setName("啥七");
        int result = userInfoMapper.delete(userInfo);
        if (result > 0){
            System.out.println(userInfo + ":该条数据删除成功!");
        }
    }

    /**
     * 通过主键进行删除,这里最多只会删除一条数据
     * 单个字段做主键时,可以直接写主键的值
     * 联合主键时,key可以是实体类,也可以是Map
     */
    @Test
    void test8(){
        int result = userInfoMapper.deleteByPrimaryKey(4L);
        if (result > 0){
            System.out.println(4L + "主键:该条数据删除成功!");
        }
    }

    /**
     * 根据主键进行更新,这里最多只会更新一条数据
     * 参数为实体类;参数实体类中字段为null的字段也会更新
     */
    @Test
    void test9(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(3L);
        userInfo.setName("王五五五五五");

        int result = userInfoMapper.updateByPrimaryKey(userInfo);
        if (result > 0){
            System.out.println(userInfo + ":该条数据更新成功!");
        }
    }

    /**
     * 根据主键进行更新
     * 只会更新参数实体类中不是null的数据
     */
    @Test
    void test10(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(2L);
        userInfo.setName("李四四四四");

        int result = userInfoMapper.updateByPrimaryKeySelective(userInfo);
        if (result > 0){
            System.out.println(userInfo + ":该条数据更新成功!");
        }
    }

如果想要增加自己写的方法,可以直接在 UserInfoMapper 中增加。

方式1:

import tk.mybatis.mapper.common.Mapper;
import java.util.List;

public interface UserInfoMapper extends Mapper<UserInfo> {

    /**
     * 自定义查询所有方法
     * @return
     */
    List<UserInfo> selectAll2();
}

对应的UserInfoMapper.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wbs.dao.UserInfoMapper">

    <!--查询所有用户信息-->
    <select id="selectAll2" resultType="com.wbs.pojo.UserInfo">
        select * from user_info
    </select>

</mapper>

方式2(基于JPA注解方式):

import tk.mybatis.mapper.common.Mapper;

public interface UserInfoMapper extends Mapper<UserInfo> {

    /**
     * 手动编写,根据名字查询用户
     * @param name
     * @return
     */
    @Select("select * from user_info where name = #{name}")
    UserInfo selectByName(String name);
}

上述两种方式均可以实现自定义扩展方法,并且互不影响,在接口中添加其他方法的时候和只用 MyBatis 效果是完全一样的,但是需要注意,在对应的 XML 中,不能出现和继承通用Mapper接口中同名的方法!

  • 多态!

    在接口中,只要不是通过注解来实现接口方法,接口是允许重名的,真正调用会使用通用 Mapper 提供的方法。

3.3、数据库映射(注解)

上述看到的是最简单的情况,其实实际使用过程中也不会更复杂,下面是更详细的映射配置。

通用 Mapper 中,默认情况下是将实体类字段按照驼峰转下划线形式的表名列名进行转换。

例如:

实体类的 userName 可以映射到表的 user_name 上。

如果想要修改默认的转换方式,可以在后续的配置中,修改 style 全局配置。

数据库映射主要涉及到一些注解和全局配置,这里会介绍所有注解,后面会有配置的介绍。

通用 Mapper 默认使用了几个简单的注解,其他 JPA 的注解默认并不支持,但是如果你开发自己的通用方法,你可以使用 JPA 注解或者引入自己的注解。

3.3.1、@NameStyle 注解(Mapper)

这个注解可以在类上进行配置,优先级高于对应的 style 全局配置。

注解支持以下几个选项:

normal,                     //原值
camelhump,                  //驼峰转下划线
uppercase,                  //转换为大写
lowercase,                  //转换为小写
camelhumpAndUppercase,      //驼峰转下划线大写形式
camelhumpAndLowercase,      //驼峰转下划线小写形式

使用时,直接在类上配置即可,例如:

@NameStyle(Style.camelhumpAndUppercase)
public class UserInfo{
}

配置该注解后,对该类和其中的字段进行转换时,会将形如 userName 的字段转换为表中的 USER_NAME 字段。

3.3.2、@Table 注解(JPA)

@Table 注解可以配置 name,catalogschema 三个属性,配置 name 属性后,直接使用提供的表名,不再根据实体类名进行转换。其他两个属性中,同时配置时,catalog 优先级高于 schema,也就是只有 catalog 会生效。

配置示例如下:

//只要这里对应数据库表名即可,也就是将UserInfoTable实体类映射到user_info数据库表
@Table(name = "user_info") 
public class UserInfoTable{
}

3.3.3、@Column 注解(JPA)

@Column 注解支持 name, insertableupdateable 三个属性。

  • name 配置映射的列名。
  • insertable 对提供的 insert 方法有效,设置为 false 就不会出现在 SQL 中。
  • updateable 对提供的 update 方法有效,设置为 false 就不会出现在 SQL 中。

配置示例如:

@Column(name = "user_name")
private String name;

除了直接映射 nameuser_name 这种用法外,在使用关键字的情况,还会有下面的用法:

//会用英文的``引起来
@Column(name = "`des`")
private String des;

对于关键字这种情况,通用 Mapper 支持自动转换,可以查看后续配置文档中的 wrapKeyword 配置。

3.3.4、@ColumnType 注解(Mapper)

这个注解提供了三个属性:

  • column属性和 @Column 中的 name 属性作用相同,但是 @Column 的优先级更高。
  • jdbcType 属性是指定字段的类型。
  • typeHandler 用于设置特殊类型处理器,常见的是枚举。

用法示例如下:

@ColumnType(
        column = "name",
        jdbcType = JdbcType.VARCHAR,
        typeHandler = StringTypeHandler.class)
private String  name;

3.3.5、@Transient 注解(JPA)

一般情况下,实体中的字段和数据库表中的字段是一一对应的,但是也有很多情况我们会在实体中增加一些额外的属性,这种情况下,就需要使用 @Transient 注解来告诉通用 Mapper 这不是表中的字段。默认情况下,只有简单类型会被自动认为是表中的字段(可以通过配置中的 useSimpleType 控制)。

注意:

这里的简单类型不包含 Java 中的8种基本类型:

byte,short,int,long,float,double,char,boolean

这是因为在类中,基本类型会有默认值,而 MyBatis 中经常会需要判断属性值是否为空,所以不要在类中使用基本类型,否则会遇到莫名其妙的错误。

对于类中的复杂对象,以及 Map,List 等属性不需要配置这个注解(映射的时候会自动排除)。

对于枚举类型作为数据库字段的情况,需要看配置中的 enumAsSimpleType 参数。

配置示例:

@Transient
private String otherThings; //非数据库表中字段

3.3.6、@Id 注解(JPA)

上面几个注解都涉及到映射。 @Id 注解和映射无关,它是一个特殊的标记,用于标识数据库中的主键字段。

正常情况下,一个实体类中至少需要一个标记 @Id 注解的字段,存在联合主键时可以标记多个。

如果表中没有主键,类中就可以不标记。但是当类中没有存在标记 @Id 注解的字段时,你可以理解为类中的所有字段是联合主键。使用所有的 ByPrimaryKey 相关的方法时,有 where 条件的地方,会将所有列作为条件(这种情况效率极低)。

配置示例:

@Id
private Integer id;

或者联合主键:

@Id
private Integer userId;

@Id
private Integer roleId;

3.3.7、@KeySql 注解(Mapper)

主键策略注解,用于配置如何生成主键。

这是通用 Mapper 的自定义注解,该注解的目的就是为替换 @GeneratedValue 注解。

关于该注解的用法可以查看 2.3 主键策略

3.3.8、@GeneratedValue 注解(JPA)

主键策略注解,用于配置如何生成主键。

由于不同类型数据库的配置不同,所以后面有一节专门介绍该注解的文档。

关于该注解的用法可以查看 2.3 主键策略

推荐使用上面的 @KeySql 注解。

3.3.9、@Version 注解(Mapper)

@Version 是实现乐观锁的一个注解,大多数人都不需要。

需要使用该注解的请看 2.4 乐观锁

3.3.10、@RegisterMapper 注解(Mapper)

为了解决通用 Mapper 中最常见的一个错误而增加的标记注解,该注解仅用于开发的通用接口,不是实体类上使用的,这里和其他注解一起介绍了。

通用Mapper-4.0 版本提供的所有通用接口上都标记了该注解,因此在使用自带的通用接口时,不需要配置 mappers 参数。

3.3.11、自定义注解

在通用 Mapper 中,可以通过 EntityHelper.getColumns(entityClass) 方法来获取实体类的全部信息。

EntityColumn 中,通过下面的代码可以获取字段上的任意注解。

//判断是否有某个注解
boolean hasVersion = column.getEntityField().isAnnotationPresent(Version.class)
//通过下面的代码可以获取注解信息
Version version = column.getEntityField().getAnnotation(Version.class);

通过这种方式,在实现自己的通用方式时,可以根据需要来增加额外的注解来实现一些其他的用途。

3.4、主键策略

首先主键策略和数据库关系很大,有些数据库支持主键自增,而有些数据库只能通过序列来获得。

新增的@KeySql 注解用于替换 @GeneratedValue 注解,因此 @KeySql 能以更简单的方式实现原来的功能,下面的示例都先使用 @KeySql 进行配置,然后在使用 @GeneratedValue,大家可以自己选择。

3.4.1、JDBC 支持通过 getGeneratedKeys 方法取回主键的情况

这种情况首先需要数据库支持自增,其次数据库提供的 JDBC 支持 getGeneratedKeys 方法。

常见的如 MySql,SqlServer 支持这种模式。

这种情况下,配置主键策略最简单。

用法如下:

@Id
@KeySql(useGeneratedKeys = true)
private Long id;

或:

@Id
@GeneratedValue(generator = "JDBC")
private Long id;

为了让大家容易理解这里配置和 MyBatis 写法的关系,大家可以看看对应生成的 XML 代码:

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    insert into user_info (id, name, des)
    values (#{id},#{name},#{des})
</insert>

3.4.2、支持自增的数据库

支持自增的数据库列表如下:

  • DB2: VALUES IDENTITY_VAL_LOCAL()
  • MYSQL: SELECT LAST_INSERT_ID()
  • SQLSERVER: SELECT SCOPE_IDENTITY()
  • CLOUDSCAPE: VALUES IDENTITY_VAL_LOCAL()
  • DERBY: VALUES IDENTITY_VAL_LOCAL()
  • HSQLDB: CALL IDENTITY()
  • SYBASE: SELECT @@IDENTITY
  • DB2_MF: SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1
  • INFORMIX: select dbinfo('sqlca.sqlerrd1') from systables where tabid=1
上述列表只是列举了比较常见的数据库,其他类似的也支持。

列表数据库名字后面对应的 SQL 是插入后取 id 的 SQL 语句

这类数据库主键策略配置示例如下:

@Id
@KeySql(dialect = IdentityDialect.DEFAULT)//DEFAULT 需要配合 IDENTITY 参数(ORDER默认AFTER)
private Integer id;

@Id
@KeySql(dialect = IdentityDialect.MYSQL)//建议直接指定数据库
private Integer id;

或:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@GeneratedValue 用法还要配合 IDENTITY 参数(以及 ORDER 参数),这个参数值需要配置为对应的数据库,就是上面列表中的名字,具体的配置方法看后面章节的内容。

配置对应的 XML 形式为:

<insert id="insertUser">
    <selectKey keyProperty="id" resultType="int" order="AFTER">
      SELECT LAST_INSERT_ID()
    </selectKey>
    insert into user_info(id, name, des)
    values (#{id},#{name},#{des})
</insert>

IDENTITY 参数以及 ORDER 参数会决定 selectKey 中的 SQL 和 order 属性的值(这里是 AFTER)。

3.4.3、通过序列和任意 SQL 获取主键值

像 Oracle 中通过序列获取主键就属于这种情况,实际上 2.3.2 中的 SQL 也可以在这里配置。

除了类似序列获取值外,还可以是获取 UUID 的 SQL 语句,例如 select uuid()

selectKey通过序列和任意SQL 获取主键的区别主要在于,前者是插入表之后才有 id 的值,后者是插入数据库前需要获取一个值作为主键。

在 Oracle 中,我们可以用下面的方式进行配置:

@Id
@KeySql(sql = "select SEQ_ID.nextval from dual", order = ORDER.BEFORE)
private Integer id;

或:

@Id
@GeneratedValue(
  strategy = GenerationType.IDENTITY,
  generator = "select SEQ_ID.nextval from dual")
private Integer id;

使用 @GeneratedValue 时也要配置一个 ORDER 全局参数,3.4.2中提到的 AFTER,在 3.4.3 中需要配置为 BEFORE,具体配置方式看后面章节的内容。

这种配置对应的 XML 代码如下:

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select SEQ_ID.nextval from dual
  </selectKey>
  insert into user_info(id, name, des)
  values (#{id},#{name},#{des})
</insert>
这种用法中,values 中必须出现主键的值,否则就插不到数据库。

除了 Oracle 这种外,还有一种更普遍的用法,就是使用 UUID

例如下面的配置:

@Id
@GeneratedValue(
    strategy = GenerationType.IDENTITY,
    generator = "select uuid()")
private String id;
注意 SQL 返回值类型和这里接收的类型(String)一致。

3.4.4、@KeySql 介绍

上面的例子中都列举了 @KeySql 方式的用法。下面全面的看看这个注解:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface KeySql {
    /**
     * 是否使用 JDBC 方式获取主键,优先级最高,设置为 true 后,不对其他配置校验
     *
     * @return
     */
    boolean useGeneratedKeys() default false;
    /**
     * 优先级第二,根据配置的数据库类型取回主键,忽略其他配置
     *
     * @return
     */
    IdentityDialect dialect() default IdentityDialect.DEFAULT;
    /**
     * 取主键的 SQL
     *
     * @return
     */
    String sql() default "";
    /**
     * 和 sql 可以配合使用,默认使用全局配置中的 ORDER
     *
     * @return
     */
    ORDER order() default ORDER.DEFAULT;
}

通过上面的注释,大家可以看到主要的 3 个参数的优先级,useGeneratedKeys 优先级最高,其次是 dialect,最后是 sql。其中 order 只对 sql 方式有效。

useGeneratedKeysdialect 相当于预设的基本用法,和数据库以及驱动紧密相关。

sqlorder 更灵活。

3.5、乐观锁

乐观锁实现中,要求一个实体类中只能有一个乐观锁字段。

3.5.1、配置 @Version

想要使用乐观锁,只需要在实体中,给乐观锁字段增加 @tk.mybatis.mapper.annotation.Version 注解。

例如:

public class UserInfo {
         private Long id;
      private String name;
        private String des;

    @Version
      private Integer version;
}

@Version 注解有一个 nextVersion 属性,默认值为默认的实现,默认实现如下:

import java.sql.Timestamp;

public class DefaultNextVersion implements NextVersion {

    @Override
    public Object nextVersion(Object current) throws VersionException {
        if (current == null) {
            throw new VersionException("当前版本号为空!");
        }
        if (current instanceof Integer) {
            return (Integer) current + 1;
        } else if (current instanceof Long) {
            return (Long) current + 1L;
        } else if (current instanceof Timestamp) {
            return new Timestamp(System.currentTimeMillis());
        } else {
            throw new VersionException("默认的 NextVersion 只支持 Integer, Long" +
                    " 和 java.sql.Timestamp 类型的版本号,如果有需要请自行扩展!");
        }
    }
}

默认实现支持 Integer, Longjava.sql.Timestamp ,如果默认实现不能满足自己的需要,可以实现自己的方法,在配置注解时指定自己的实现即可。

支持的方法

  • delete
  • deleteByPrimaryKey
  • updateByPrimaryKey
  • updateByPrimaryKeySelective
  • updateByExample
  • updateByExampleSelective

这些方法在执行时会更新乐观锁字段的值或者使用乐观锁的值作为查询条件。

3.5.2、需要注意的地方

在使用乐观锁时,由于通用 Mapper 是内置的实现,不是通过 拦截器 方式实现的,因此当执行上面支持的方法时,如果版本不一致,那么执行结果影响的行数可能就是 0。但是这种情况下也不会报错!

所以在 Java6,7中使用时,你需要自己在调用方法后进行判断是否执行成功。

Java8+ 中,可以通过默认方法来增加能够自动报错(抛异常)的方法,例如:

public interface MyMapper<T> extends Mapper<T> {
  
  default int deleteWithVersion(T t){
    int result = delete(t);
    if(result == 0){
      throw new RuntimeException("删除失败!");
    }
    return result;
  }
  
  default int updateByPrimaryKeyWithVersion(Object t){
    int result = updateByPrimaryKey(t);
    if(result == 0){
      throw new RuntimeException("更新失败!");
    }
    return result;
  }
  
  //...
}

通过增加上面这种默认方法就能实现。

3.6、全局主键

3.6.1、通用 Mapper @KeySql 注解 genId 方法详解

为了方便使用全局主键(例如:Vesta 是一款通用的ID产生器,互联网俗称统一发号器),通用 Mapper 4.0.2 版本增加了新的控制主键生成的策略。

@KeySql 注解增加了下面的方法:

/** 
 * Java 方式生成主键,可以和发号器一类的服务配合使用
 * @return 
 */
Class<? extends GenId> genId() default GenId.NULL.class;

使用该功能的时候,需要配置 genId 属性。 由于生成主键的方式通常和使用环境有关,因此通用 Mapper 没有提供默认的实现。

GenId 接口如下:

public interface GenId<T> {
    class NULL implements GenId {
        @Override
        public Object genId(String table, String column) {
            throw new UnsupportedOperationException();
        }
    }

    T genId(String table, String column);
}

通过接口方法可以看出,在生成 Id 时,可以得到当前的表名和列名,我们可以使用这两个信息生成和表字段相关的 Id 信息。也可以完全不使用这两个信息,生成全局的 Id。

使用 genId 方式时,字段的值可以回写!

3.6.2、使用 UUID

1. 实现 GenId

使用 UUID 方式时,首先我们需要提供一个能产生 UUID 的实现类:

public class UUIdGenId implements GenId<String> {
    @Override
    public String genId(String table, String column) {
        return UUID.randomUUID().toString().replace("-","");//将"-"替换成""
    }
}

这个实现类就是简单的返回了一个 String 类型的值,并且没有处理 UUID 中的 -

2. 配置 genId

例如有如下表(mysql数据库):

CREATE TABLE `user` (
  `id` VARCHAR(100) NOT NULL COMMENT '主键id',
  `name` VARCHAR(255) DEFAULT NULL COMMENT '姓名',
  `des` VARCHAR(255) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8mb4

对应如下实体类:

@Table(name = "user")
@Data
public class User implements Serializable {

    @Id
    @KeySql(genId = UUIdGenId.class)
    private String id;

    private String name;

    private String des;

    public User(String name, String des){
        this.name = name;
        this.des = des;
    }
}

我们只需要在注解中配置 @KeySql(genId = UUIdGenId.class) 即可,需要注意的是,如果你使用了 @KeySql 提供的其他方式,genId 就不会生效,genId 是所有方式中优先级最低的。

3、实现接口

@org.apache.ibatis.annotations.Mapper
public interface UserMapper extends Mapper<User> {

}

4、测试

        @Autowired
    UserMapper userMapper;

    @Test
    void test12() {
        String[][] strings = new String[][]{{"小红", "红红"},{"小明", "明明"},{"小黑", "嘿嘿"}};
        for (int i = 0; i < strings.length; i++) {
            userMapper.insertSelective(new User(strings[i][0],strings[i][1]));
        }
    }

输出的部分日志如下:

main] com.wbs.dao.UserMapper.insertSelective   : ==>  Preparing: INSERT INTO user ( id,name,des ) VALUES( ?,?,? ) 
main] com.wbs.dao.UserMapper.insertSelective   : ==> Parameters: 3725de243ea6486099af2f7b885eadf4(String), 小红(String), 红红(String)
main] com.wbs.dao.UserMapper.insertSelective   : <==    Updates: 1
main] com.wbs.dao.UserMapper.insertSelective   : ==>  Preparing: INSERT INTO user ( id,name,des ) VALUES( ?,?,? ) 
main] com.wbs.dao.UserMapper.insertSelective   : ==> Parameters: aac02f922a874bd9aa3b24458be1e031(String), 小明(String), 明明(String)
main] com.wbs.dao.UserMapper.insertSelective   : <==    Updates: 1
main] com.wbs.dao.UserMapper.insertSelective   : ==>  Preparing: INSERT INTO user ( id,name,des ) VALUES( ?,?,? ) 
main] com.wbs.dao.UserMapper.insertSelective   : ==> Parameters: 804ec904384f4c2ebedf6f8c29049bba(String), 小黑(String), 嘿嘿(String)
main] com.wbs.dao.UserMapper.insertSelective   : <==    Updates: 1

查看数据库表中数据:
SpringBoot集成MyBatis通用Mapper4

3.6.3、简单的全局时序ID

1. 实现 GenId

使用 UUID 方式时,首先我们需要提供一个能产生 UUID 的实现类:

public class SimpleGenId implements GenId<Long> {
    private Long    time;
    private Integer seq;

    @Override
    public synchronized Long genId(String table, String column) {
        long current = System.currentTimeMillis();
        if (time == null || time != current) {
            time = current;
            seq = 1;
        } else if (current == time) {
            seq++;
        }
        return (time << 20) | seq;
    }
}

这是一个简单的实现,通过同步方法保证唯一,不考虑任何特殊情况,不要用于生产环境。

2. 配置 genId

例如有如下表(hsqldb 数据库):

create table country (
  id          bigint NOT NULL PRIMARY KEY,
  countryname varchar(32),
  countrycode VARCHAR(2)
);

对应如下实体:

@Data
public class Country {
    @Id
    @KeySql(genId = SimpleGenId.class)
    private Long id;
    private String countryname;
    private String countrycode;

    public Country() {
    }

    public Country(String countryname, String countrycode) {
        this.countryname = countryname;
        this.countrycode = countrycode;
    }
}

我们只需要在注解中配置 @KeySql(genId = SimpleGenId.class) 即可,需要注意的是,如果你使用了 @KeySql 提供的其他方式,genId 就不会生效,genId 是所有方式中优先级最低的。

3. 测试

        @Autowired
    CountryMapper countryMapper;

    /**
     * 测试Country,使用简单的全局时序ID
     */
    @Test
    void test13() {
        String[][] strings = new String[][]{{"中国", "00"},{"美国", "11"},{"法国", "22"}};
        for (int i = 0; i < strings.length; i++) {
            countryMapper.insertSelective(new Country(strings[i][0],strings[i][1]));
        }
    }

输出的部分日志如下:

c.wbs.dao.CountryMapper.insertSelective  : ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? ) 
c.wbs.dao.CountryMapper.insertSelective  : ==> Parameters: 1719101302947971073(Long), 中国(String), 00(String)
c.wbs.dao.CountryMapper.insertSelective  : <==    Updates: 1
c.wbs.dao.CountryMapper.insertSelective  : ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? ) 
c.wbs.dao.CountryMapper.insertSelective  : ==> Parameters: 1719101303041294337(Long), 美国(String), 11(String)
c.wbs.dao.CountryMapper.insertSelective  : <==    Updates: 1
c.wbs.dao.CountryMapper.insertSelective  : ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? ) 
c.wbs.dao.CountryMapper.insertSelective  : ==> Parameters: 1719101303103160321(Long), 法国(String), 22(String)
c.wbs.dao.CountryMapper.insertSelective  : <==    Updates: 1

查看数据库表中数据:

SpringBoot集成MyBatis通用Mapper4

3.6.4、基于 Vesta 的实现

通过前面两个例子应该能看出,通过不同的 GenId 实现就能切换不同的 ID 生成方式,ID 的类型需要保证一致。

这里提供一个能应用于生产环境的思路,由于和具体环境有关,因此这里没有直接可用的代码。

示例代码:

public class VestaGenId implement GenId<Long> {
   public Long genId(String table, String column){
       //ApplicationUtil.getBean 需要自己实现
       IdService idService = ApplicationUtil.getBean(IdService.class);
       return idService.genId();
   }
}

这个代码也很简单,由于注解只能配置类,不能注入,因此 VestaGenId 实现中不能直接注入 IdService,需要通过静态方法从 Spring Context 中获取,获取后就可以正常使用了。

  • 特殊的地方

    使用 genId 时,在和数据库交互前,ID 值就已经生成了,由于这个 ID 和数据库的机制无关,因此一个实体中可以出现任意个 使用 genId 方式的 @KeySql 注解,这些注解可以指定不同的实现方法。

3.6.4、InsertListMapper 特殊支持

通用 Mapper 提供了好几个 InsertListMapper 接口,最早提供的都是和数据库相关的方法,所以在 Id 策略上都有和数据库相关的限制。

最新增加的 tk.mybatis.mapper.additional.insert.InsertListMapper 接口是一个和数据库无关的方法,他不支持任何的主键策略。但是有了 genId 方式后,这个接口增加了对 @KeySqlgenId 方法的支持。

student表(mysql数据库):

CREATE TABLE `student` (
  `id` VARCHAR(100) NOT NULL COMMENT '主键id',
  `name` VARCHAR(255) DEFAULT NULL COMMENT '姓名',
  `des` VARCHAR(255) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8mb4

对应如下实体类:

@Table(name = "student")
@Data
public class Student implements Serializable {

    @Id
    @KeySql(genId = UUIdGenId.class)
    private String id;

    private String name;

    private String des;

    public User(String name, String des){
        this.name = name;
        this.des = des;
    }
}

使用UUID,生成id:

public class UUIdGenId implements GenId<String> {
    @Override
    public String genId(String table, String column) {
        return UUID.randomUUID().toString().replace("-","");//将"-"替换成""
    }
}

接口定义:

import com.wbs.pojo.Student;
import tk.mybatis.mapper.additional.insert.InsertListMapper;

public interface StudentMapper extends InsertListMapper<Student> {

}

测试代码:

        @Autowired
    StudentMapper studentMapper;

    @Test
    void test14(){
        String[][] strings = new String[][]{{"法外狂徒", "张三"},{"万人揍", "李华"},{"爪哇", "一宿君"}};
        List<Student> studentList = new ArrayList<Student>(strings.length);
        for (int i = 0; i < strings.length; i++) {
            studentList.add(new Student(strings[i][0], strings[i][1]));
        }
        studentMapper.insertList(studentList);
    }

输出日志如下:

com.wbs.dao.StudentMapper.insertList     : ==>  Preparing: INSERT INTO student( id,name,des ) VALUES ( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) 
com.wbs.dao.StudentMapper.insertList     : ==> Parameters: 1e009a31fddd4a448c60c219d74677f9(String), 法外狂徒(String), 张三(String), ef2e076bbdca4dac8770108a3884bce4(String), 万人揍(String), 李华(String), c63ec975056b489590acc869c71fe315(String), 爪哇(String), 一宿君(String)
com.wbs.dao.StudentMapper.insertList     : <==    Updates: 3

查询数据库表数据:

SpringBoot集成MyBatis通用Mapper4

通过 genId 方式可以批量插入,并且可以回写 ID。

上一篇:HashMap的底层原理和线程安全的替代方案


下一篇:Java中BigDecimal详解及应用