对象关系映射

二、对象关系映射

通过上一章的文档集成好通用 Mapper 后,就可以继续看这里了。

通用 Mapper 使用 JPA 注解和自己提供的注解来实现对象关系映射,由于本章包含了很多细节,所以通过简单的示例先引领入门,然后在一步步深入去看详细的配置。

本章包含下面的内容,请按照顺序阅读。

 

2.1 简单示例

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

数据库有如下表:

CREATE TABLE `country` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `countryname` varchar(255) DEFAULT NULL COMMENT '名称',
  `countrycode` varchar(255) DEFAULT NULL COMMENT '代码',
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=10011 DEFAULT CHARSET=utf8 COMMENT='国家信息';

对应的 Java 实体类型如下:

public class Country {
    @Id
    private Integer id;
    private String  countryname;
    private String  countrycode;

    //省略 getter 和 setter
}

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

提醒:如果实体类中没有一个标记 @Id 的字段,当你使用带有 ByPrimaryKey 的方法时,所有的字段会作为联合主键来使用,也就会出现类似 where id = ? and countryname = ? and countrycode = ?的情况。

第四章会介绍代码生成器,可以自动生成上面的实体和下面的接口代码

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

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

import tk.mybatis.mapper.common.Mapper;

public interface CountryMapper extends Mapper<Country> {
}

只要配置 MyBatis 时能注册或者扫描到该接口,该接口提供的方法就都可以使用。

该接口默认继承的方法如下:

  • selectOne
  • select
  • selectAll
  • selectCount
  • selectByPrimaryKey
  • 方法太多,省略其他...

从 MyBatis 中获取该接口后就可以直接使用:

//从 MyBatis 或者 Spring 中获取 countryMapper,然后调用 selectAll 方法
List<Country> countries = countryMapper.selectAll();
//根据主键查询
Country country = countryMapper.selectByPrimaryKey(1);
//或者使用对象传参,适用于1个字段或者多个字段联合主键使用
Country query = new Country();
query.setId(1);
country = countryMapper.selectByPrimaryKey(query);

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

1. 使用纯接口注解方式时

import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.common.Mapper;

public interface CountryMapper extends Mapper<Country> {
    @Select("select * from country where countryname = #{countryname}")
    Country selectByCountryName(String countryname);
}

这里只是举了个简单的例子,可以是很复杂的查询。

2. 如果使用 XML 方式,需要提供接口对应的 XML 文件

例如提供了 CountryMapper.xml 文件,内容如下:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tk.mybatis.sample.mapper.CountryMapper">
    <select id="selectByCountryName" resultType="tk.mybatis.model.Country">
        select * from country where countryname = #{countryname}
    </select>
</mapper>

在接口中添加对应的方法:

import tk.mybatis.mapper.common.Mapper;

public interface CountryMapper extends Mapper<Country> {
    Country selectByCountryName(String countryname);
}

在接口中添加其他方法的时候和只用 MyBatis 是完全一样的,但是需要注意,在对应的 XML 中,不能出现和继承接口中同名的方法!

多态!

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

例如在上面 CountryMapper 中提供一个带分页的 selectAll 方法:

public interface CountryMapper extends Mapper<Country> {
    List<Country> selectAll(RowBounds rowBounds);
}

在 Java 8 的接口中通过默认方法还能增加一些简单的间接调用方法,例如:

public interface CountryMapper extends Mapper<Country> {
    //这个示例适合参考实现对乐观锁方法封装
    default void updateSuccess(Country country){
        Assert.assertEquals(1, updateByPrimaryKey(country));
    }
}

在不熟悉 Java 语言和 MyBatis 特性前,不要轻易使用!

2.2 数据库映射

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

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

例如

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

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

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

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

2.2.1@NameStyle 注解(Mapper)

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

注解支持以下几个选项:

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

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

@NameStyle(Style.camelhumpAndUppercase)
public class Country

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

2.2.2 @Table 注解(JPA)

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

配置示例如下:

@Table(name = "sys_user")
public class User

将 User 实体映射到 sys_user 表。

2.2.3 @Column 注解(JPA)

@Column 注解支持 nameinsertable 和 updateable 三个属性。

name 配置映射的列名。

insertable 对提供的 insert 方法有效,如果设置 false 就不会出现在 SQL 中。

updateable 对提供的 update 方法有效,设置为 false 后不会出现在 SQL 中。

配置示例如:

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

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

@Column(name = "`order`")
private String order;

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

2.2.4 @ColumnType 注解(Mapper)

这个注解提供的 column属性和 @Column 中的 name 作用相同。但是 @Column 的优先级更高。

除了 name 属性外,这个注解主要提供了 jdbcType 属性和 typeHandler 属性。

jdbcType 用于设置特殊数据库类型时指定数据库中的 jdbcType

typeHandler 用于设置特殊类型处理器,常见的是枚举。

用法示例如下:

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

2.2.5 @Transient 注解(JPA)

一般情况下,实体中的字段和数据库表中的字段是一一对应的,但是也有很多情况我们会在实体中增加一些额外的属性,这种情况下,就需要使用 @Transient 注解来告诉通用 Mapper 这不是表中的字段。

默认情况下,只有简单类型会被自动认为是表中的字段(可以通过配置中的 useSimpleType 控制)。

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

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

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

对于类中的复杂对象,以及 Map,List 等属性不需要配置这个注解。

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

配置示例:

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

2.2.6 @Id 注解(JPA)

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

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

如果表中没有主键,类中就可以不标记。

当类中没有存在标记 @Id 注解的字段时,你可以理解为类中的所有字段是联合主键。使用所有的 ByPrimaryKey 相关的方法时,有 where 条件的地方,会将所有列作为条件。

配置示例:

@Id
private Integer id;

或者联合主键:

@Id
private Integer userId;
@Id
private Integer roleId;

2.2.7 @KeySql 注解

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

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

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

2.2.8 @GeneratedValue 注解(JPA)

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

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

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

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

2.2.9 @Version 注解(Mapper)

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

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

2.2.10 @RegisterMapper 注解

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

4.0 版本提供的所有通用接口上都标记了该注解,因此自带的通用接口时,不需要配置 mappers 参数,该注解的具体用法会在 第五章 扩展通用接口 中介绍。

2.2.11 自定义注解

这部分提供给想要开发通用方法的朋友,如果只是使用,不需要阅读这部分内容。 

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

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

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

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

2.3 主键策略

在 2.2 中介绍了所有其他注解的作用和用法,由于主键策略比较复杂,单独用一节来阐述。

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

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

2.3.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 country (id, countryname, countrycode)
    values (#{id},#{countryname},#{countrycode})
</insert>

SqlServer 中使用时,需要设置 id 的 insertable=false

2.3.2 支持自增的数据库

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

  • DB2VALUES IDENTITY_VAL_LOCAL()
  • MYSQLSELECT LAST_INSERT_ID()
  • SQLSERVERSELECT SCOPE_IDENTITY()
  • CLOUDSCAPEVALUES IDENTITY_VAL_LOCAL()
  • DERBYVALUES IDENTITY_VAL_LOCAL()
  • HSQLDBCALL IDENTITY()
  • SYBASESELECT @@IDENTITY
  • DB2_MFSELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1
  • INFORMIXselect dbinfo('sqlca.sqlerrd1') from systables where tabid=1

这个列表只是列举了比较常见的数据库,其他类似的也支持。

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

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

@Id
//DEFAULT 需要配合 IDENTITY 参数(ORDER默认AFTER)
@KeySql(dialect = IdentityDialect.DEFAULT)
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="insertAuthor">
    <selectKey keyProperty="id" resultType="int" order="AFTER">
      SELECT LAST_INSERT_ID()
    </selectKey>
    insert into country (id, countryname, countrycode)
    values (#{id},#{countryname},#{countrycode})
</insert>

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

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

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

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

2.3.3 和 2.3.2 的区别主要在于,2.3.2 是插入表之后才有 id 的值,2.3.3 是插入数据库前需要获取一个值作为主键。

在 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 全局参数,2.3.2 中提到的 AFTER,在 2.3.3 中需要配置为 BEFORE,具体配置方式看后面章节的内容。

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

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select SEQ_ID.nextval from dual
  </selectKey>
  insert into country (id, countryname, countrycode)
  values (#{id},#{countryname},#{countrycode})
</insert>

这种用法中,values 中必须出现主键的值,否则就插不到数据库。

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

例如下面的配置:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,generator = "select uuid()")
private String id;

注意 SQL 返回值类型和这里接收的类型(String)一致。

2.3.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 方式有效。

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

sql 和 order 更灵活。

2.4 乐观锁

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

配置 @Version

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

例如:

public class User {
 	private Long id;
  	private String name;
    //...
    @Version
  	private Integer version;
  	//setter and getter
}

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

package tk.mybatis.mapper.version;

import java.sql.Timestamp;

/**
 * @author liuzh
 * @since 3.5.0
 */
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 类型的版本号,如果有需要请自行扩展!");
        }
    }

}

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

支持的方法

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

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

需要注意的地方

在使用乐观锁时,由于通用 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;
  }
  
  //...
  
}

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

 

2.5 通用 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 方式时,字段的值可以回写!

示例一,使用 UUID

完整测试代码:https://github.com/abel533/Mapper/tree/master/base/src/test/java/tk/mybatis/mapper/base/genid

1. 实现 GenId

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

public class UUIdGenId implements GenId<String> {
    @Override
    public String genId(String table, String column) {
        return UUID.randomUUID().toString();
    }
}

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

2. 配置 genId

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

create table user (
  id   varchar(64) NOT NULL PRIMARY KEY,
  name varchar(32),
  code VARCHAR(2)
);

对应如下实体:

public class User {
    @Id
    @KeySql(genId = UUIdGenId.class)
    private String id;
    private String name;
    private String code;

    public User() {
    }

    public User(String name, String code) {
        this.name = name;
        this.code = code;
    }
	//省略 setter 和 getter
}

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

3. 测试

@Test
public void testUUID(){
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        for (int i = 0; i < countries.length; i++) {
            User user = new User(countries[i][0], countries[i][1]);
            Assert.assertEquals(1, mapper.insert(user));
            Assert.assertNotNull(user.getId());
            System.out.println(user.getId());
        }
    } finally {
        sqlSession.close();
    }
}

输出的部分日志如下:

DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@3eb7fc54]
DEBUG [main] - ==>  Preparing: INSERT INTO user ( id,name,code ) VALUES( ?,?,? )
DEBUG [main] - ==> Parameters: 94ee80ac-d156-412d-b63e-b75b49026874(String), Angola(String), AO(String)
DEBUG [main] - <==    Updates: 1
94ee80ac-d156-412d-b63e-b75b49026874
DEBUG [main] - ==>  Preparing: INSERT INTO user ( id,name,code ) VALUES( ?,?,? )
DEBUG [main] - ==> Parameters: 0f21aab9-0637-4b7f-ade9-fe5bc4cf1693(String), Afghanistan(String), AF(String)
DEBUG [main] - <==    Updates: 1
0f21aab9-0637-4b7f-ade9-fe5bc4cf1693

示例二,简单的全局时序ID

完整测试代码:https://github.com/abel533/Mapper/tree/master/base/src/test/java/tk/mybatis/mapper/base/genid

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

对应如下实体:

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;
    }
	//省略 setter 和 getter
}

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

3. 测试

@Test
public void testGenId(){
    SqlSession sqlSession = getSqlSession();
    try {
        CountryMapper mapper = sqlSession.getMapper(CountryMapper.class);
        for (int i = 0; i < countries.length; i++) {
            Country country = new Country(countries[i][0], countries[i][1]);
            Assert.assertEquals(1, mapper.insert(country));
            Assert.assertNotNull(country.getId());
            System.out.println(country.getId());
        }
    } finally {
        sqlSession.close();
    }
}

输出的部分日志如下:

DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@96def03]
DEBUG [main] - ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? )
DEBUG [main] - ==> Parameters: 1598437075106922497(Long), Angola(String), AO(String)
DEBUG [main] - <==    Updates: 1
1598437075106922497
DEBUG [main] - ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? )
DEBUG [main] - ==> Parameters: 1598437075176128513(Long), Afghanistan(String), AF(String)
DEBUG [main] - <==    Updates: 1
1598437075176128513
DEBUG [main] - ==>  Preparing: INSERT INTO country ( id,countryname,countrycode ) VALUES( ?,?,? )
DEBUG [main] - ==> Parameters: 1598437075178225665(Long), Albania(String), AL(String)
DEBUG [main] - <==    Updates: 1
1598437075178225665

示例三,基于 Vesta 的实现

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

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

Vesta 地址:https://gitee.com/robertleepeak/vesta-id-generator

示例代码:

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 中获取,获取后就可以正常使用了。

对 Spring 熟悉的人可以很容易理解 ApplicationUtil.getBean 方法。 如果对 Spring 不了解,可以看看 http://sb33060418.iteye.com/blog/2372874 这里的SpringContextHolder

特殊的地方

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

还没完,InsertListMapper 特殊支持

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

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

下面是一个示例。

测试地址:https://github.com/abel533/Mapper/tree/master/extra/src/test/java/tk/mybatis/mapper/additional/insertlist

使用前面 示例一 中的 User 实体和表。

接口定义:

import tk.mybatis.mapper.additional.insert.InsertListMapper;

public interface UserMapper extends InsertListMapper<User> {

}

测试代码:

@Test
public void testInsertList() {
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> userList = new ArrayList<User>(countries.length);
        for (int i = 0; i < countries.length; i++) {
            userList.add(new User(countries[i][0], countries[i][1]));
        }
        Assert.assertEquals(countries.length, mapper.insertList(userList));
        for (User user : userList) {
            Assert.assertNotNull(user.getId());
            System.out.println(user.getId());
        }
    } finally {
        sqlSession.close();
    }
}

部分日志:

DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@778d1062]
DEBUG [main] - ==>  Preparing: INSERT INTO user ( id,name,role ) VALUES ( ?,?,? ) ,
( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) ,
( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) ,
( ?,?,? ) , ( ?,?,? ) , ( ?,?,? ) , ( ?,?,? )
DEBUG [main] - ==> Parameters:
1b8511b7-304a-47a7-abec-c2f38e498e69(String), Angola(String), AO(String),
2d31db7e-5d4c-4997-9db3-44163018a3c7(String), Afghanistan(String), AF(String),
31ac04d7-cb61-45b2-9b7d-d75e82a65529(String), Albania(String), AL(String),
39d77534-bf9a-47d4-aaa0-63d32c2d4562(String), Algeria(String), DZ(String),
5186f2e5-57b1-4837-8e06-0392f70e6dbe(String), Andorra(String), AD(String),
4ccab2fb-db59-4588-ae90-c33e9acc5a84(String), Anguilla(String), AI(String),

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值