Mybatis源码学习(24)-KeyGenerator、SelectKeyGenerator、Jdbc3KeyGenerator

一、简介

  在Mybatis中,insert语句执行时,可以返回自动产生的主键,这里便是使用KeyGenerator来完成的。本篇内容就来分析Mybatis中的主键生成策略是怎么样起作用的。
  首先,不同的数据库产品对应的主键生成策略不一样,主要分为两类:一类是在执行insert 语句之前必须明确指定主键的,比如: Oracle 、DB2 等数据库;一类是可以不指定主键,而在插入过程中由数据库自动生成自增主键,比如:MYSQL, Postgresql等数据库。
  在KeyGenerator中,针对不同的数据库产品,提供了不同的方法进行处理。其中,processBefore()方法,在执行insert之前执行,一般用于Oracle 、DB2 等数据库;processAfter()方法,在执行insert之后执行,一般用于MYSQL, Postgresql等数据库。(注:后续 分析源码发现,其实两个方法只是明确是在insert执行前或执行后执行,和数据库没有关系)

//KeyGenerator.java
/**
 * 主键生成器接口,有三个实现类:
 * 1、 {@link Jdbc3KeyGenerator}<br>
 * 2、{@link NoKeyGenerator}<br>
 * 3、{@link SelectKeyGenerator}<br>
 * @author Clinton Begin
 */
public interface KeyGenerator {
  /**
   * 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录,
   * 如Oracle、DB2,KeyGenerator提供了processBefore()方法。
   * @param executor
   * @param ms
   * @param stmt
   * @param parameter
   */
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
  /**
   * 针对自增主键的表,在插入时不需要主键,而是在插入过程自动获取一个自增的主键,
   * 比如MySQL,Postgresql,KeyGenerator提供了processAfter()方法
   * @param executor
   * @param ms
   * @param stmt
   * @param parameter
   */
  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

}
二、KeyGenerator实现类

  Mybatis提供了三个KeyGenerator接口的实现类,分别是NoKeyGenerator、Jdbc3KeyGenerator、SelectKeyGenerator。其中,

  • NoKeyGenerator:默认空实现,不对主键单独处理;
  • Jdbc3KeyGenerator:主要用于数据库的自增主键,比如 MySQL、PostgreSQL;
  • SelectKeyGenerator:主要用于数据库不支持自增主键的情况,比如 Oracle、DB2;

类图:
在这里插入图片描述

三、实现类Jdbc3KeyGenerator

  Jdbc3KeyGenerator主要用于支持主键自增的数据库,比如MySQL、PostgreSQL、SQL Server等。

1、useGeneratedKeys属性的用法

  首先,如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置到目标属性上就 OK 了。例如:

<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

注:需要把Author表中主键对应的字段设置成自动生成的列类型

2、Jdbc3KeyGenerator类详解

  Jdbc3KeyGenerator实现类就是处理那些支持主键自增的数据库的,主要用来支持上述提到的通过useGeneratedKeys属性配置主键生成策略的实现。

  1. 变量
    Jdbc3KeyGenerator实现类中定义了一个静态的Jdbc3KeyGenerator变量INSTANCE,供全局使用,即全局共享这一个静态变量。
//Jdbc3KeyGenerator.java
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
  1. processBefore()方法
    因为Jdbc3KeyGenerator实现类是处理那些支持主键自增的数据库的,所以在insert语句执行前,不做任何处理,即该方法为空实现。
  2. processAfter()方法
    processAfter()方法是通过调用processBatch()方法实现想要的业务逻辑。调用processBatch()方法前,首先通过getParameters()方法处理参数,然后再调用processBatch()方法。
//Jdbc3KeyGenerator.java
  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, getParameters(parameter));
  }
  1. getParameters()方法
    将传入的parameter实参转换成Collection类型对象。
//Jdbc3KeyGenerator.java
  private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }
  1. processBatch()方法
    processBatch()方法主要实现了一下逻辑:
    1、获取数据库自动生成的主键
    2、获取主键对应的属性名称
    3、检测数据库生成的主键的列数与keyProperties属性指定的列数是否匹配
    4、for循环,处理参数。因为在insert语句中,可能存在同时添加多条数据的情况,每次循环即处理一条添加的记录。
    5、在for循环过程中,首先根据参数,获取对应的typeHandler,然后根据typeHandler获取主键字段对应的值,最后通过MetaObject的setValue()方法,把参数及其参数值写入到对应对象中。
//Jdbc3KeyGenerator.java
public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
    ResultSet rs = null;
    try {
    	//获取数据库自动生成的主键,如果没有生成主键,则返回结采集为空
      rs = stmt.getGeneratedKeys();
      final Configuration configuration = ms.getConfiguration();
      final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      //获得keyProperties属性指定的属性名称,它表示主键对应的属性名称
      final String[] keyProperties = ms.getKeyProperties();
      //获取ResultSet的元数据信息
      final ResultSetMetaData rsmd = rs.getMetaData();
      TypeHandler<?>[] typeHandlers = null;
      //检测数据库生成的主键的列数与keyProperties属性指定的列数是否匹配
      if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
        for (Object parameter : parameters) {
          // there should be one row for each statement (also one for each parameter)
          if (!rs.next()) {
            break;
          }
          final MetaObject metaParam = configuration.newMetaObject(parameter);
          if (typeHandlers == null) {//获取主键字段分别对应的TypeHandler
            typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
          }
          //将生成的主键设直到用户传入的参数的对应位置
          populateKeys(rs, metaParam, keyProperties, typeHandlers);
        }
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    } finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
  }
  1. getTypeHandlers()方法
    获取对应的TypeHandler。
//Jdbc3KeyGenerator.java
private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
    TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
    for (int i = 0; i < keyProperties.length; i++) {
      if (metaParam.hasSetter(keyProperties[i])) {
        TypeHandler<?> th;
        try {
          Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
          th = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
        } catch (BindingException e) {
          th = null;
        }
        typeHandlers[i] = th;
      }
    }
    return typeHandlers;
  }
  1. populateKeys()方法
    将生成的主键设置到用户传入的参数的对应位置。
//Jdbc3KeyGenerator.java
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    for (int i = 0; i < keyProperties.length; i++) {
      String property = keyProperties[i];
      TypeHandler<?> th = typeHandlers[i];
      if (th != null) {
        Object value = th.getResult(rs, i + 1);
        metaParam.setValue(property, value);
      }
    }
  }
四、实现类SelectKeyGenerator

  SelectKeyGenerator实现类主要用于数据库不支持自增主键的情况,比如 Oracle、DB2等。

1、 <selectKey>节点用法

  对于不支持自动生成类型的数据库或可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键。这里有一个简单(甚至很傻)的示例,它可以生成一个随机 ID(你最好不要这么做,但这里展示了 MyBatis 处理问题的灵活性及其所关心的广度)。在下面的示例中,selectKey 元素中的语句将会首先运行,Author 的 id 会被设置,然后插入语句会被调用。这可以提供给你一个与数据库中自动生成主键类似的行为,同时保持了 Java 代码的简洁。

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
2、 SelectKeyGenerator类详解

  对于不支持自动生成自增主键的数据库,例如Oracle 数据库,用户可以利用Mybatis提供的SelectkeyGenerator 来生成主键, SelectkeyGenerator 也可以实现类似于Jdbc3KeyGenerator 提供的、获取数据库自动生成的主键的功能。

  1. 变量
//SelectKeyGenerator.java
  /**
	 * <selectKey>节点,解析过程中,生成id时,使用的默认后缀
	 */
  public static final String SELECT_KEY_SUFFIX = "!selectKey";
  /**
   * 标识<selectKey>节点中定义的SQL语句是在insert语句之前执行还是之后执行
   */
  private final boolean executeBefore;
  /**
   * <selectKey>节点中定义的SQL语句所对应的MappedStatement对象。
   * 该MappedStatement对象是在解析<selectKey>节点时创建的。
   * 该SQL语句用于获取insert语句中使用的主键。
   */
  private final MappedStatement keyStatement;
  1. 构造函数
//SelectKeyGenerator.java
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
    this.executeBefore = executeBefore;
    this.keyStatement = keyStatement;
  }
  1. processBefore()方法、processAfter()方法
    在processBefore()方法和processAfter()方法的实现都是调用processGeneratedKeys()方法。通过标识<selectKey>节点中定义的SQL语句是在insert语句之前执行还是之后执行的变量executeBefore来确定,执行processBefore()或processAfter()方法。(根据方法的逻辑,支持自增主键的数据库,理论上应该也可以使用该方法,进行主键生成,未验证)
//SelectKeyGenerator.java
@Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (executeBefore) {
      processGeneratedKeys(executor, ms, parameter);
    }
  }

  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (!executeBefore) {
      processGeneratedKeys(executor, ms, parameter);
    }
  }
  1. processGeneratedKeys()方法
    执行<selectKey>节点中配置的SQL语句,获取insert语句中用到的主键并映射成对象,然后按照配置,将主键对象中对应的属性设置到用户参数中。
//SelectKeyGenerator.java
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
    	//获取<selectKey>节点的keyProperties配置的属性名称,它表示主键对应的属性
        String[] keyProperties = keyStatement.getKeyProperties();
        final Configuration configuration = ms.getConfiguration();
        final MetaObject metaParam = configuration.newMetaObject(parameter);
        if (keyProperties != null) {
          // Do not close keyExecutor.
          // The transaction will be closed by parent executor.
          //创建Executor对象,并执行keyStatement字段中记录的SQL语句,并得到主键对象
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
          List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
          if (values.size() == 0) {
            throw new ExecutorException("SelectKey returned no data.");            
          } else if (values.size() > 1) {
            throw new ExecutorException("SelectKey returned more than one value.");
          } else {
            MetaObject metaResult = configuration.newMetaObject(values.get(0));
            if (keyProperties.length == 1) {
              if (metaResult.hasGetter(keyProperties[0])) {
                setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
              } else {
                // no getter for the property - maybe just a single value object
                // so try that
                setValue(metaParam, keyProperties[0], values.get(0));
              }
            } else {
              //处理主键有多列的情况,其实现是从主键对象中取出指定属性,并设直到用户参数的对应属性中
              handleMultipleProperties(keyProperties, metaParam, metaResult);
            }
          }
        }
      }
    } catch (ExecutorException e) {
      throw e;
    } catch (Exception e) {
      throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
    }
  }
  1. setValue()方法
    通过MetaObject实例对象,实现为指定对象的字段赋值。
//SelectKeyGenerator.java
private void setValue(MetaObject metaParam, String property, Object value) {
    if (metaParam.hasSetter(property)) {
      metaParam.setValue(property, value);
    } else {
      throw new ExecutorException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");
    }
  }
  1. handleMultipleProperties()方法
    处理主键有多列的情况,其实现是从主键对象中取出指定属性,并设直到用户参数的对应属性中。
//SelectKeyGenerator.java
private void handleMultipleProperties(String[] keyProperties,
      MetaObject metaParam, MetaObject metaResult) {
    String[] keyColumns = keyStatement.getKeyColumns();
      
    if (keyColumns == null || keyColumns.length == 0) {
      // no key columns specified, just use the property names
      for (String keyProperty : keyProperties) {
        setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
      }
    } else {
      if (keyColumns.length != keyProperties.length) {
        throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
      }
      for (int i = 0; i < keyProperties.length; i++) {
        setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
      }
    }
  }
发布了48 篇原创文章 · 获赞 3 · 访问量 3109

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/104017987