diff --git a/changes/1.5.0.md b/changes/1.5.0.md index 09855468b443a1bc3eef83bc7c646235530eceee..b062dcad38066b48261d381f30539e88c99b27ae 100644 --- a/changes/1.5.0.md +++ b/changes/1.5.0.md @@ -78,6 +78,7 @@ Seata 鏄竴娆惧紑婧愮殑鍒嗗竷寮忎簨鍔¤В鍐虫柟妗堬紝鎻愪緵楂樻€ц兘鍜岀畝鍗� - [[#3236](https://github.com/seata/seata/pull/3236)] 浼樺寲鎵ц瑙i攣鎿嶄綔鐨勬潯浠讹紝鍑忓皯涓嶅繀瑕佺殑store鎿嶄綔銆� - [[#3485](https://github.com/seata/seata/pull/3485)] 浼樺寲 ConfigurationFactory 涓棤鐢ㄧ殑try/catch - [[#3505](https://github.com/seata/seata/pull/3505)] 浼樺寲GlobalTransactionScanner绫讳腑鏃犵敤鐨刬f鍒ゆ柇 + - [[#3544](https://github.com/seata/seata/pull/3544)] 浼樺寲鏃犳硶閫氳繃Statement#getGeneratedKeys鏃讹紝鍙兘鑾峰彇鍒版壒閲忔彃鍏ョ殑绗竴涓富閿殑闂 - [[#3549](https://github.com/seata/seata/pull/3549)] 缁熶竴涓嶅悓琛ㄤ腑鐨剎id瀛楁鐨勯暱搴� - [[#3551](https://github.com/seata/seata/pull/3551)] 璋冨ぇRETRY_DEAD_THRESHOLD鐨勫€间互鍙婅缃垚鍙厤缃� diff --git a/changes/en-us/1.5.0.md b/changes/en-us/1.5.0.md index d054bd46313774dcdea60faa40390b177631f86b..d939f0cd645104116d054c690c8392ca79fa5b62 100644 --- a/changes/en-us/1.5.0.md +++ b/changes/en-us/1.5.0.md @@ -175,10 +175,9 @@ - [[#3236](https://github.com/seata/seata/pull/3236)] optimize the conditions for executing unlocking - [[#3485](https://github.com/seata/seata/pull/3485)] optimize useless codes in ConfigurationFactory - [[#3505](https://github.com/seata/seata/pull/3505)] optimize useless if judgments in the GlobalTransactionScanner class + - [[#3544](https://github.com/seata/seata/pull/3544)] optimize the get pks by auto when auto generated keys is false - [[#3549](https://github.com/seata/seata/pull/3549)] unified the length of xid in scripts - [[#3551](https://github.com/seata/seata/pull/3551)] make RETRY_DEAD_THRESHOLD bigger and configurable - - ### test diff --git a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/mysql/MySQLInsertExecutor.java b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/mysql/MySQLInsertExecutor.java index 03fc704eb421261755f02226c358fa1a006a1218..9b73dff4a78239f293f64a62f964bc8087576464 100644 --- a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/mysql/MySQLInsertExecutor.java +++ b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/mysql/MySQLInsertExecutor.java @@ -32,6 +32,7 @@ import io.seata.sqlparser.util.JdbcConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; @@ -41,6 +42,7 @@ import java.util.Objects; import java.util.HashSet; import java.util.Set; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; /** * @author jsbxyyx @@ -55,6 +57,13 @@ public class MySQLInsertExecutor extends BaseInsertExecutor implements Defaultab */ public static final String ERR_SQL_STATE = "S1009"; + /** + * The cache of auto increment step of database + * the key is the db's resource id + * the value is the step + */ + public static final Map<String, BigDecimal> RESOURCE_ID_STEP_CACHE = new ConcurrentHashMap<>(8); + /** * Instantiates a new Abstract dml base executor. * @@ -127,8 +136,17 @@ public class MySQLInsertExecutor extends BaseInsertExecutor implements Defaultab // specify Statement.RETURN_GENERATED_KEYS to // Statement.executeUpdate() or Connection.prepareStatement(). if (ERR_SQL_STATE.equalsIgnoreCase(e.getSQLState())) { - LOGGER.warn("Fail to get auto-generated keys, use 'SELECT LAST_INSERT_ID()' instead. Be cautious, statement could be polluted. Recommend you set the statement to return generated keys."); - genKeys = statementProxy.getTargetStatement().executeQuery("SELECT LAST_INSERT_ID()"); + LOGGER.error("Fail to get auto-generated keys, use 'SELECT LAST_INSERT_ID()' instead. Be cautious, " + + "statement could be polluted. Recommend you set the statement to return generated keys."); + int updateCount = statementProxy.getUpdateCount(); + ResultSet firstId = genKeys = statementProxy.getTargetStatement().executeQuery("SELECT LAST_INSERT_ID()"); + + // If there is batch insert + // do auto increment base LAST_INSERT_ID and variable `auto_increment_increment` + if (updateCount > 1 && canAutoIncrement(pkMetaMap)) { + firstId.next(); + return autoGeneratePks(new BigDecimal(firstId.getString(1)), autoColumnName, updateCount); + } } else { throw e; } @@ -171,4 +189,39 @@ public class MySQLInsertExecutor extends BaseInsertExecutor implements Defaultab // mysql default keyword the logic not support. (sample: insert into test(id, name) values(default, 'xx')) throw new NotSupportYetException(); } + + protected Map<String, List<Object>> autoGeneratePks(BigDecimal cursor, String autoColumnName, Integer updateCount) throws SQLException { + BigDecimal step = BigDecimal.ONE; + String resourceId = statementProxy.getConnectionProxy().getDataSourceProxy().getResourceId(); + if (RESOURCE_ID_STEP_CACHE.containsKey(resourceId)) { + step = RESOURCE_ID_STEP_CACHE.get(resourceId); + } else { + ResultSet increment = statementProxy.getTargetStatement().executeQuery("SHOW VARIABLES LIKE 'auto_increment_increment'"); + + increment.next(); + step = new BigDecimal(increment.getString(2)); + RESOURCE_ID_STEP_CACHE.put(resourceId, step); + } + + List<Object> pkValues = new ArrayList<>(); + for (int i = 0; i < updateCount; i++) { + pkValues.add(cursor); + cursor = cursor.add(step); + } + + Map<String, List<Object>> pkValuesMap = new HashMap<>(1, 1.001f); + pkValuesMap.put(autoColumnName,pkValues); + return pkValuesMap; + } + + protected boolean canAutoIncrement(Map<String, ColumnMeta> primaryKeyMap) { + if (primaryKeyMap.size() != 1) { + return false; + } + + for (ColumnMeta pk : primaryKeyMap.values()) { + return pk.isAutoincrement(); + } + return false; + } } diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java index 32b9a56273923d22cf7daf3eeb05f632c2bd0829..023fe03ab3da2a16e497366d8ee51efa3b8ea133 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java @@ -15,11 +15,17 @@ */ package io.seata.rm.datasource.exec; +import com.mysql.jdbc.ResultSetImpl; +import com.mysql.jdbc.util.ResultSetUtil; import io.seata.common.exception.ShouldNeverHappenException; +import io.seata.common.util.ReflectionUtil; import io.seata.rm.datasource.ConnectionProxy; +import io.seata.rm.datasource.DataSourceProxy; import io.seata.rm.datasource.PreparedStatementProxy; import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.exec.mysql.MySQLInsertExecutor; +import io.seata.rm.datasource.mock.MockDataSource; +import io.seata.rm.datasource.mock.MockResultSet; import io.seata.rm.datasource.sql.struct.ColumnMeta; import io.seata.rm.datasource.sql.struct.Row; import io.seata.rm.datasource.sql.struct.TableMeta; @@ -35,6 +41,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -73,12 +82,18 @@ public class MySQLInsertExecutorTest { private HashMap<String,Integer> pkIndexMap; @BeforeEach - public void init() { + public void init() throws SQLException { ConnectionProxy connectionProxy = mock(ConnectionProxy.class); when(connectionProxy.getDbType()).thenReturn(JdbcConstants.MYSQL); + when(connectionProxy.getDataSourceProxy()).thenReturn(new DataSourceProxy(new MockDataSource())); statementProxy = mock(PreparedStatementProxy.class); when(statementProxy.getConnectionProxy()).thenReturn(connectionProxy); + when(statementProxy.getTargetStatement()).thenReturn(statementProxy); + + MockResultSet resultSet = new MockResultSet(statementProxy); + resultSet.mockResultSet(Arrays.asList("Variable_name", "Value"), new Object[][]{{"auto_increment_increment", "1"}}); + when(statementProxy.getTargetStatement().executeQuery("SHOW VARIABLES LIKE 'auto_increment_increment'")).thenReturn(resultSet); StatementCallback statementCallback = mock(StatementCallback.class); sqlInsertRecognizer = mock(SQLInsertRecognizer.class); @@ -565,6 +580,20 @@ public class MySQLInsertExecutorTest { Assertions.assertFalse(insertExecutor.checkPkValuesForSinglePk(pkValues, false)); } + @Test + public void test_autoGeneratePks() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = MySQLInsertExecutor.class.getDeclaredMethod("autoGeneratePks", new Class[]{BigDecimal.class, String.class, Integer.class}); + method.setAccessible(true); + Object resp = method.invoke(insertExecutor, BigDecimal.ONE, "ID", 3); + + Assertions.assertNotNull(resp); + Assertions.assertTrue(resp instanceof Map); + + Map<String, List> map = (Map<String, List>) resp; + Assertions.assertEquals(map.size(), 1); + Assertions.assertEquals(map.get("ID").size(), 3); + } + private List<String> mockInsertColumns() { List<String> columns = new ArrayList<>(); columns.add(ID_COLUMN);