Seata-AT模式:MySQL自增ID的場景下推薦在 Mybatis 中使用 useGeneratedKeys
一、上下文
Seata AT 模式在 構建 insert 操作的 afterImage 時,如果是自增 ID 的情況下,需要獲取剛插入記錄的自增 ID 值是什麼。在《Seata-AT 模式+TDDL:排查 構建 Insert 操作的 afterImage 時執行 SELECT LAST_INSERT_ID()報錯》 中有描述因為上下文環境中沒有啟用 useGeneratedKeys ,複用 insert 操作對應的PreparedStatement
在執行 SELECT LAST_INSERT_ID()
時,因 TDDL 內還會執行 insert 遺留的三個佔位符對應引數設定的邏輯,而導致了報錯。對於報錯之處statementProxy.getTargetStatement().executeQuery("SELECT LAST_INSERT_ID()");
來說以下兩種修復方案似乎都可以考慮:
執行 SELECT LAST_INSERT_ID()時,新建一個 PreparedStatement
若複用 PreparedStatement
,也可用clearParameters()
方法將 insert 時設定的三個引數清除掉
當然從 Seata AT 下的原始碼中梳理可知,採用useGeneratedKeys
,可完全規避 Seata 內建的SELECT LAST_INSERT_ID()
相關邏輯的執行。
二、什麼是 useGeneratedKeys
useGeneratedKeys
參數列示支援返回自動生成主鍵,當然這個特性需要 JDBC 驅動程式相容(一些驅動可能不相容)。也依賴支援自動生成記錄主鍵的資料庫,如 MySQL 和 SQL Server。如果引數值設定為 true,則進行 INSERT 操作後,資料庫自動生成的主鍵會填充到 Java 實體屬性中。
三、 mybatis 中使用 useGeneratedKeys
3.1 理論基礎
在 Mybatis 中,useGeneratedKeys
該屬性僅對 <update>
和 <insert>
標籤有用,屬性值為 true 時,MyBatis 使用 JDBC Statement 物件的 getGeneratedKeys()方法來取出由資料庫內部生成的鍵值,例如 MySQL 自增主鍵。另外配套的還有兩個引數,描述如下:
keyProperty
:該屬性僅對<update>
和<insert>
標籤有用,用於將資料庫自增主鍵或者<insert>
標籤中<selectKey>
標籤返回的值填充到實體的屬性中,如果有多個屬性,則使用逗號分隔。keyColumn
:該屬性僅對<update>
和<insert>
標籤有用,透過生成的鍵值設定表中的列名,這個設定僅在某些資料庫(例如 PostgreSQL)中是必需的,當主鍵列不是表中的第一列時需要設定,如果有多個欄位,則使用逗號分隔。
在 useGeneratedKeys
的場景下,keyColumn
配置為 DB 中的自增 ID 的欄位名稱,keyProperty
配置為 Java 物件中對應的自增 ID 的屬性名稱,關係如下圖:
keyProperty與keyColumn的關係圖示(來自網路).png
從 Mybatis 內的實現來看, MyBatis 內的主鍵生成器是 KeyGenerator
,MyBatis 中提供了 3 種KeyGenerator
:
Jdbc3KeyGenerator
Jdbc3KeyGenerator
實現的processAfter
方法,是將資料庫自動生成的主鍵填充到 Java 實體屬性中,這種用法只能是在有自增主鍵的資料庫中NoKeyGenerator
無自增主鍵,處理邏輯是空 SelectKeyGenerator
在 SelectKeyGenerator
的原始碼實現中就是執行 select last_insert_id() as id 這條 sql 語句,獲得結果並賦值給 id
在 MySQL + useGeneratedKeys
的場景下,使用到的就是Jdbc3KeyGenerator
。
3.2 程式碼示例
實體類 public class Stock {
private Long id;
private Long skuId;
private Integer stockNum;
private Date gmtCreated;
...
//省略 getxxx setxxxinterface mapper 中的 insert 方法 @Insert("INSERT INTO tstock (`sku_id`,`stock_num`,`gmt_created`) VALUES(#{stock.skuId},#{stock.stockNum},#{stock.gmtCreated});")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
public int insert(@Param("stock") Stock stock);
其他上下文非核心,忽略不提。
3.2 結果
基於以上示例進行測試,悲劇的是 insert stock 記錄後,stock 的 id 屬性還是 null,跟預期不一致。
先把正確的方式同步給大家,需調整配置 將keyProperty = "id"
調整為keyProperty = "stock.id"
,完整示例如下:
@Insert("INSERT INTO tstock (`sku_id`,`stock_num`,`gmt_created`) VALUES(#{stock.skuId},#{stock.stockNum},#{stock.gmtCreated});")
@Options(useGeneratedKeys = true, keyProperty = "stock.id", keyColumn = "id")
public int insert(@Param("stock") Stock stock);
四、梳理原始碼查詢原因
Mybatis 中useGeneratedKeys
將資料庫自動生成的主鍵填充到 Java 實體自增 ID 屬性中的邏輯就發生在org.apache.ibatis.executor.statement.PreparedStatementHandler#update
中的keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject)
環節中。
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
//這裡,將資料庫自動生成的主鍵填充到Java實體自增 ID 屬性中
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
在第二部分有提到 Jdbc3KeyGenerator
實現了關鍵的processAfter
方法,出問題的地方就是在keyGenerator#processAfter
這個方法的子邏輯方法populateKeys
中,debug 的堆疊如下:
populateKeys:142, Jdbc3KeyGenerator (org.apache.ibatis.executor.keygen)
processBatch:79, Jdbc3KeyGenerator (org.apache.ibatis.executor.keygen)
processAfter:57, Jdbc3KeyGenerator (org.apache.ibatis.executor.keygen)
update:50, PreparedStatementHandler (org.apache.ibatis.executor.statement)
update:74, RoutingStatementHandler (org.apache.ibatis.executor.statement)
doUpdate:50, SimpleExecutor (org.apache.ibatis.executor)
update:117, BaseExecutor (org.apache.ibatis.executor)
update:76, CachingExecutor (org.apache.ibatis.executor)
update:198, DefaultSqlSession (org.apache.ibatis.session.defaults)
insert:185, DefaultSqlSession (org.apache.ibatis.session.defaults)
在 populateKeys
方法中根據除錯情況確認了是給 id 賦值的環節,出了問題,方法和關鍵環節的註釋說明如下:
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];// 居然是 null
if (th != null) {
Object value = th.getResult(rs, i + 1);
metaParam.setValue(property, value);
}
}
}
populateKeys
方法中出問題的時候關鍵引數資訊如下,看到 TypeHandlers 是 null 後就意識到不對了,對 Mybatis 瞭解的話會知道 沒有 typeHandlers 的話,是無法完成屬性賦值的。
keyProperties = {String[1]@14656} ["id"]
> 0 = "id"
typeHandlers = {TypeHandler[1]@15132}
> All elements are null
補充一下TypeHandler
的資訊,TypeHandler
是型別轉換器,在 Mybatis 中用於實現 Java 型別和 JDBC 型別的相互轉換。Mybatis 使用prepareStatement
來進行引數設定的時候,需要透過TypeHandler
將傳入的 Java 引數設定成合適的 JDBC 型別引數,這個過程實際上是透過呼叫prepareStatement
不同的set
方法實現的;在獲取結果返回之後,也需要將返回的結果轉換成我們需要的 java 型別。
所以,接下來只要找出為什麼 typeHandlers 為空,為什麼 沒有建立 id 對應的 TypeHandler,應該就能找到解決辦法了。上層方法processBatch
中搜集關鍵線索,關鍵邏輯如下:
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (typeHandlers == null) {
// 獲取 typeHandlers
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
}
// 透過typeHandlers給屬性賦值
populateKeys(rs, metaParam, keyProperties, typeHandlers);
getTypeHandlers
返回的typeHandlers
是空,直接進入到問題發生的關鍵邏輯方法org.apache.ibatis.reflection.wrapper.MapWrapper#getSetterType
中,程式碼邏輯以及除錯時上下文引數如下:
@Override
public Class<?> getSetterType(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return Object.class;
} else {
return metaValue.getSetterType(prop.getChildren());
}
} else {
//程式碼執行到此處,map中,有`[stock, param1]`,沒有 ID
if (map.get(name) != null) {
return map.get(name).getClass();
} else {
return Object.class;
}
}
}
從原始碼除錯情況,發現了問題進入了 else 邏輯,且 map 變數裡有 [stock, param1]
(param1
非本篇關注點,可忽略),但是沒有 id
;id
是stock
的屬性,所以正常情況下,應該是要執行屬性相關的操作,從方法名也可看出是跟 metaObject.metaObjectForProperty
有關。根據經驗盲猜一下,id 是stock
的屬性,所以是不是應該使用 stock.id
呢 ?
調整配置,將keyProperty
的值設定為stock.id
,完整情況如下:
@Insert("INSERT INTO tstock (`sku_id`,`stock_num`,`gmt_created`) VALUES(#{stock.skuId},#{stock.stockNum},#{stock.gmtCreated});")
@Options(useGeneratedKeys = true, keyProperty = "stock.id", keyColumn = "id")
public int insert(@Param("stock") Stock stock);
再次除錯,可以看到org.apache.ibatis.reflection.wrapper.MapWrapper#getSetterType
中,關鍵變數PropertyTokenizer
的值有明顯的變化,如下:prop = {PropertyTokenizer@14306}
name = "stock" indexedName = "stock" index = null children = "id"
因為引數值有變化,所以org.apache.ibatis.reflection.wrapper.MapWrapper#getSetterType
的執行情況跟上一次進入 else 環節不同,(結合下邊程式碼看)就是:
先獲取 stock 的 MetaObject;因為 prop.getIndexedName() = "stock" 再獲取 stock 中 id 屬性的 class 資訊,之後透過此資訊即可構建 TypeHandler,除錯時留意這個方法遞迴呼叫
//1. 先獲取stock的MetaObject;prop.getIndexedName() = "stock"
MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return Object.class;
} else {
// 2. 再獲取stock 中 id屬性的class資訊,之後透過此資訊即可構建TypeHandler,這是個遞迴呼叫哈
return metaValue.getSetterType(prop.getChildren());
}
至此,從相關原始碼中探究了為何將keyProperty = "id"
調整為keyProperty = "stock.id"
的邏輯的真相。讀者老師的環境也可能有所不同,需結合自己的元件程式碼上下文進行確認。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2931755/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- bluebird 這個npm包在什麼場景下推薦使用?NPM
- 深入分析Mybatis 使用useGeneratedKeys獲取自增主鍵MyBatis
- Android中Activity的啟動模式(LaunchMode)和使用場景Android模式
- MyBatis 中 @Param 註解的四種使用場景MyBatis
- 深度UPLIFT模型在騰訊金融使用者增長場景中的應用模型
- 在MySQL中建立實現自增的序列(Sequence)MySql
- MyBatis 返回(批次)新增資料的自增idMyBatis
- metersphere 介面自動化中sql場景使用SQL
- MySQL 中的自增主鍵MySql
- 深度模型DNN在個性化推薦場景中的應用模型DNN
- Android設計模式——策略模式之原始碼使用場景(三)Android設計模式原始碼
- JMeter MQTT 在連線測試場景中的使用JMeterMQQT
- MySQL和Elasticsearch使用場景MySqlElasticsearch
- 個性化海報在愛奇藝影片推薦場景中的實踐
- Redis 中 BitMap 的使用場景Redis
- 機器學習在銷售報價單的產品推薦場景中的作用機器學習
- Apache RocketMQ 5.0 在Stream場景的儲存增強ApacheMQ
- Android LaunchMode使用場景Android
- 楊玉基:知識圖譜在美團推薦場景中的應用
- Android設計模式之——單例模式之原始碼使用場景(一)Android設計模式單例原始碼
- mysql的自增id的一個問題MySql
- RabbitMQ 使用場景、安裝、工作模式MQ模式
- 揭秘3D點雲在自動駕駛中的應用場景3D自動駕駛
- android 啟動模式應用場景Android模式
- Mybatis在Spring中的使用(三)MyBatisSpring
- Debias 技術在金融推薦場景下的應用
- Android之什麼場景該使用單例模式總結Android單例模式
- mysql 資料庫自增id 的總結MySql資料庫
- mysql與redis的區別與使用場景MySqlRedis
- 場景在關卡設計中的比重
- JS 中 this 在各個場景下的指向JS
- mysql自增和orcale自增MySql
- mybatis獲取自增id的值MyBatis
- MyBatis實現MySQL表欄位及結構的自動增刪MyBatisMySql
- JMeter MQTT 在訂閱與釋出測試場景中的使用JMeterMQQT
- MyBatis 中 @Param 註解的四種使用場景,最後一種經常被人忽略!MyBatis
- RocketMQ 在多 IDC 場景以及多隔離區場景下的實踐MQ
- 深入瞭解MySQL中的自增主鍵MySql