這篇文章研究下 Mybatis 配置主鍵回顯相關功能。
本篇文章將以以下幾個問題切入:
- Mybatis 如何 配置主鍵自增回顯?
- JDBC 主鍵回顯用法?
- 對於不支持自增主鍵數據庫,Mybatis 有怎麼解決這個問題?
- Mybatis 有哪幾種主鍵生成方式?
例子
- JDBC 自增主鍵例子
對於數據庫支持自增主鍵的庫,例如:Mysql:
Connection conn = DriverManager.getConnection(url, "root", "123456");
String[] columnNames = {"ids", "name"};
PreparedStatement stmt = conn.prepareStatement(sql, columnNames);
stmt.setString(1, "test jdbc3 ");
stmt.executeUpdate();
ResultSet rs = stmt.getGeneratedKeys();
int id = 0;
if (rs.next()) {
id = rs.getInt(1);
System.out.println("----------" + id);
}
即當使用 prepareStatement 或者 Statement時候,可以通過getGeneratedKeys
獲取 當條插入語句的自增而成的主鍵。
當然還有其他幾種方式,原理是 數據庫端返回一個LAST_INSERT_ID
,這個跟auto_increment_id
強相關。
其他幾種方式可以參考oracle文檔:https://docs.oracle.com/cd/E17952_01/connector-j-en/connector-j-usagenotes-last-insert-id.html
Mybtis 對於數據庫主鍵回顯,主要分爲兩種,一種是數據庫支持自增主鍵字段,另一種是數據庫不支持方式即手動指定方式。
- 數據庫支持主鍵回顯,例如
Mysql,postgresql
使用Jdbc3KeyGenerator
進行數據庫回顯:
在 insert 中配置useGeneratedKeys="true" keyColumn="id" keyProperty="id"
,即當插入或者批量插入後,成功後即可返回配置id:
<insert id="insertWithGenertoorKey" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into department (name) values (#{department.name})
</insert>
- 數據庫不支持主鍵回顯,例如
oracle,DB2
。則使用selectKey
先查出最大id,而後進行插入操作:
<insert id="insertWithSelectKeyGenertoorKey">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT if(max(id) is null,1,max(id)+10) as newId FROM department
</selectKey>
insert into department (id, name) values (#{id}, #{department.name})
</insert>
分析
下面以 上面三個用法爲基礎,分析Mybatis 主鍵自增回顯原理。
當 Mybatis 解析 xml節點是,讀到 insert
有配置時,會判斷是否 有配置 useGeneratedKeys
,如果有則會使用 Jdbc3KeyGenerator
作爲sql回顯,否則會以 NoKeyGenerator
作爲主鍵回顯。
當執行插入數據操作時,執行以下邏輯:
- 首先會進入
MapperMethod
的execute
方法,判斷執行類型 - 當上一步判斷是
INSERT
時,則會 進入DefaultSqlSession
的insert
方法,隨後進入 其update
方法。 - 進入
Executor
的update
方法,配置完ErrorContext
後,進入doUpdate
方法:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
Mybatis
對KeyGenerator
使用是在PreparedStatementHandler
的update
中:
@Override
public int update(Statement statement) throws SQLException {
// 獲取 preparedStatement
PreparedStatement ps = (PreparedStatement) statement;
// 執行 語句
ps.execute();
// 獲取影響行
int rows = ps.getUpdateCount();
// 獲取參數
Object parameterObject = boundSql.getParameterObject();
// 獲取 KeyGenerator,自增則通過 jdbc獲取,否則就通過selectKey 查詢獲取
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
// 回填 配置星到 參數中
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
Mybatis 對於 KeyGenerator
主要集中在上面的update
代碼中,下面看看 三種 KeyGenerator
含義:
KeyGenerator
KeyGenerator
接口 中 只有 兩個方法:
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
processBefore
:主要用於 插入前操作,即獲取和設置主鍵(不支持主鍵自增)
processAfter
:用於插入後對主鍵進行回顯到參數中
- NoKeyGenerator
這個類 是對KeyGenerator
的空實現,主要是不配置generatorKey
,所以processAfter
和processBefore
事實上不需要進行任何操作 - Jdbc3KeyGenerator
使用 JDBC 方式獲取自增主鍵,其processBefore
是空實現,只實現了processAfter
:
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
try (ResultSet rs = stmt.getGeneratedKeys()) {
// 獲取自增主鍵
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
// 設值
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}
上面代碼主要就是獲取自增主鍵並且設值:
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter) throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// 多參數或者單參數使用 @Param Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// 批量插入 操作 Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
} else {
// 沒有 用 @Param註解的單參數 Single param without @Param
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}
從上面看到,Mybatis
通過判斷三種情況對 數據進行回填,從而使用不同回填,具體不在分析。
Jdbc mysql 連接驅動 中 getGeneratedKeys
獲取有以下幾個分析點:
- 爲什麼 支持 id 回顯? 通過 返回的
LAST_INSERT_ID
返回。 - 爲什麼 批量插入可以 給所有對象返還id? 通過獲取最後一個id,並且知道影響行數遍歷設值
- 爲什麼批量
insert on duplicate key update
不能回填所有id? 因爲insert on duplicate key update
這種 方式 有多種情況,分別會返回0(存在但數值一致),1(插入),2(更新), 不是固定的1,所以不能通過遍歷設值。
具體可以看看這篇文章,關於 Jdbc Mysql 驅動的 getGeneratedKeys
: 深入分析Mybatis 使用useGeneratedKeys獲取自增主鍵
- 如果數據庫不支持 自增主鍵,
Mybatis
提供一種SelectKey
方式,即先查出,再將id填充進參數進行插入操作:
@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);
}
}
// 查詢 初結果並設定
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
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
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);
}
}
對於 SelectKeyGenerator
方式進行插入,則需要在 配置 SelectKey
時候指定 order
, 如果沒有指定,則默認爲 BEFORE
:
order="BEFORE"
, BEFORE
則爲在 插入前設值,即 上述代碼中 executeBefore
爲 true
。
processGeneratedKeys
方法主要意思爲 獲取一個 Executor
,而後對 當前 MappedStatement
進行查詢操作,最終返回到 List
的value中,並設值到 parameter
中。
以上,就是 Mybatis
中 KeyGenerator
主鍵回顯原理。
覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,一起研究Mybatis: