一、簡介
在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屬性配置主鍵生成策略的實現。
- 變量
Jdbc3KeyGenerator實現類中定義了一個靜態的Jdbc3KeyGenerator變量INSTANCE,供全局使用,即全局共享這一個靜態變量。
//Jdbc3KeyGenerator.java
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
- processBefore()方法
因爲Jdbc3KeyGenerator實現類是處理那些支持主鍵自增的數據庫的,所以在insert語句執行前,不做任何處理,即該方法爲空實現。 - 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));
}
- 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;
}
- 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
}
}
}
}
- 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;
}
- 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 提供的、獲取數據庫自動生成的主鍵的功能。
- 變量
//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;
- 構造函數
//SelectKeyGenerator.java
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
this.executeBefore = executeBefore;
this.keyStatement = keyStatement;
}
- 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);
}
}
- 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);
}
}
- 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() + ".");
}
}
- 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]));
}
}
}