diff --git a/changes/en-us/develop.md b/changes/en-us/develop.md index dcf297f77fa..da1c53be175 100644 --- a/changes/en-us/develop.md +++ b/changes/en-us/develop.md @@ -15,6 +15,7 @@ Add changes here for all PR submitted to the develop branch. - [[#5245](https://github.com/seata/seata/pull/5245)] fix the incomplete dependency of distribution module - [[#5239](https://github.com/seata/seata/pull/5239)] fix `getConfig` throw `ClassCastException` when use JDK proxy - [[#5281](https://github.com/seata/seata/pull/5281)] parallel request handle throw IndexOutOfBoundsException +- [[#5287](https://github.com/seata/seata/pull/5287)] fix auto-increment of pk columns in PostgreSQL in AT mode ### optimize: - [[#5208](https://github.com/seata/seata/pull/5208)] optimize throwable getCause once more @@ -46,6 +47,7 @@ Thanks to these contributors for their code commits. Please report an unintended - [wangliang181230](https://github.com/wangliang181230) - [GoodBoyCoder](https://github.com/GoodBoyCoder) - [a364176773](https://github.com/a364176773) +- [isharpever](https://github.com/isharpever) Also, we receive many valuable issues, questions and advices from our community. Thanks for you all. diff --git a/changes/zh-cn/develop.md b/changes/zh-cn/develop.md index 74646d2bb0b..238f6492182 100644 --- a/changes/zh-cn/develop.md +++ b/changes/zh-cn/develop.md @@ -15,6 +15,7 @@ - [[#5245](https://github.com/seata/seata/pull/5245)] 修复不完整的distribution模块依赖 - [[#5239](https://github.com/seata/seata/pull/5239)] 修复当使用JDK代理时,`getConfig` 方法获取部分配置时抛出 `ClassCastException` 异常的问题 - [[#5281](https://github.com/seata/seata/pull/5281)] 修复并行rm请求处理时数组索引越界问题 +- [[#5287](https://github.com/seata/seata/pull/5287)] 修复AT模式下pgsql的主键列自增的问题 ### optimize: - [[#5208](https://github.com/seata/seata/pull/5208)] 优化多次重复获取Throwable#getCause问题 @@ -46,6 +47,7 @@ - [wangliang181230](https://github.com/wangliang181230) - [GoodBoyCoder](https://github.com/GoodBoyCoder) - [a364176773](https://github.com/a364176773) +- [isharpever](https://github.com/isharpever) 同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。 diff --git a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/postgresql/PostgresqlInsertExecutor.java b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/postgresql/PostgresqlInsertExecutor.java index 5610b86f0cd..9f6ff0f8ef9 100644 --- a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/postgresql/PostgresqlInsertExecutor.java +++ b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/postgresql/PostgresqlInsertExecutor.java @@ -18,22 +18,26 @@ import io.seata.common.exception.ShouldNeverHappenException; import io.seata.common.loader.LoadLevel; import io.seata.common.loader.Scope; +import io.seata.common.util.CollectionUtils; import io.seata.common.util.StringUtils; import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.exec.BaseInsertExecutor; import io.seata.rm.datasource.exec.StatementCallback; import io.seata.rm.datasource.sql.struct.ColumnMeta; +import io.seata.sqlparser.SQLInsertRecognizer; import io.seata.sqlparser.SQLRecognizer; import io.seata.sqlparser.struct.Defaultable; import io.seata.sqlparser.struct.Sequenceable; import io.seata.sqlparser.struct.SqlMethodExpr; import io.seata.sqlparser.struct.SqlSequenceExpr; import io.seata.sqlparser.struct.SqlDefaultExpr; +import io.seata.sqlparser.util.ColumnUtils; import io.seata.sqlparser.util.JdbcConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -60,9 +64,55 @@ public PostgresqlInsertExecutor(StatementProxy statementProxy, StatementCallback super(statementProxy, statementCallback, sqlRecognizer); } + /** + * 1. If the insert columns are not empty and do not contain any pk columns, + * it means that there is no pk value in the insert rows, then all the pk values should come from auto-increment. + *

+ * 2. The pk value exists in insert rows. The possible situations are: + *

+ * + * @return {@link Map}<{@link String}, {@link List}<{@link Object}>> + * @throws SQLException the sql exception + */ @Override public Map> getPkValues() throws SQLException { - return getPkValuesByColumn(); + List pkColumnNameList = getTableMeta().getPrimaryKeyOnlyName(); + Map> pkValuesMap = new HashMap<>(pkColumnNameList.size()); + + // first obtain the existing pk value from the insert rows (if exists) + if (!containsColumns() || containsAnyPk()) { + pkValuesMap.putAll(getPkValuesByColumn()); + } + // other from auto-increment + for (String columnName : pkColumnNameList) { + if (!pkValuesMap.containsKey(columnName)) { + pkValuesMap.put(columnName, getGeneratedKeys(columnName)); + } + } + return pkValuesMap; + } + + /** + * Whether the insert columns contain any pk columns + * + * @return true: contain at least one pk column. false: do not contain any pk columns + */ + public boolean containsAnyPk() { + SQLInsertRecognizer recognizer = (SQLInsertRecognizer)sqlRecognizer; + List insertColumns = recognizer.getInsertColumns(); + if (CollectionUtils.isEmpty(insertColumns)) { + return false; + } + List pkColumnNameList = getTableMeta().getPrimaryKeyOnlyName(); + if (CollectionUtils.isEmpty(pkColumnNameList)) { + return false; + } + List newColumns = ColumnUtils.delEscape(insertColumns, getDbType()); + return pkColumnNameList.stream().anyMatch(pkColumn -> newColumns.contains(pkColumn) + || CollectionUtils.toUpperList(newColumns).contains(pkColumn.toUpperCase())); } @Override diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PostgresqlInsertExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PostgresqlInsertExecutorTest.java index 198604f52ff..1acad8d40ab 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PostgresqlInsertExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PostgresqlInsertExecutorTest.java @@ -29,7 +29,13 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import java.util.*; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -145,6 +151,145 @@ public void testInsertDefault_ByDefault_MultiPk() throws Exception { Assertions.assertEquals(pkValuesMap.get(USER_ID_COLUMN), pkValuesAutoUserId); } + @Test + public void testGetPkValues_SinglePk() throws SQLException { + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + + List pkColumns = new ArrayList<>(); + pkColumns.add(ID_COLUMN); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + + // mock pk values from insert rows + Map> mockPkValuesFromColumn = new HashMap<>(); + mockPkValuesFromColumn.put(ID_COLUMN, Collections.singletonList(PK_VALUE_ID + 1)); + doReturn(mockPkValuesFromColumn).when(insertExecutor).getPkValuesByColumn(); + + // mock pk values from auto increment + List mockPkValuesAutoGenerated = Collections.singletonList(PK_VALUE_ID); + doReturn(mockPkValuesAutoGenerated).when(insertExecutor).getGeneratedKeys(ID_COLUMN); + + // situation1: insert columns are empty + List columns = new ArrayList<>(); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(columns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(true); + Assertions.assertIterableEquals(mockPkValuesFromColumn.entrySet(), insertExecutor.getPkValues().entrySet()); + + // situation2: insert columns contain the pk column + columns = new ArrayList<>(); + columns.add(ID_COLUMN); + columns.add(USER_NAME_COLUMN); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(columns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(false); + Assertions.assertIterableEquals(mockPkValuesFromColumn.entrySet(), insertExecutor.getPkValues().entrySet()); + + // situation3: insert columns are not empty and do not contain the pk column + columns = new ArrayList<>(); + columns.add(USER_NAME_COLUMN); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(columns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(false); + Assertions.assertIterableEquals( + Collections.singletonMap(ID_COLUMN, mockPkValuesAutoGenerated).entrySet(), + insertExecutor.getPkValues().entrySet()); + } + + @Test + public void testGetPkValues_MultiPk() throws SQLException { + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + + List pkColumns = new ArrayList<>(); + pkColumns.add(ID_COLUMN); + pkColumns.add(USER_ID_COLUMN); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + + // mock all pk values from insert rows + Map> mockAllPkValuesFromColumn = new HashMap<>(); + mockAllPkValuesFromColumn.put(ID_COLUMN, Collections.singletonList(PK_VALUE_ID + 1)); + mockAllPkValuesFromColumn.put(USER_ID_COLUMN, Collections.singletonList(PK_VALUE_USER_ID + 1)); + doReturn(mockAllPkValuesFromColumn).when(insertExecutor).getPkValuesByColumn(); + + // mock pk values from auto increment + List mockPkValuesAutoGenerated_ID = Collections.singletonList(PK_VALUE_ID); + doReturn(mockPkValuesAutoGenerated_ID).when(insertExecutor).getGeneratedKeys(ID_COLUMN); + List mockPkValuesAutoGenerated_USER_ID = Collections.singletonList(PK_VALUE_USER_ID); + doReturn(mockPkValuesAutoGenerated_USER_ID).when(insertExecutor).getGeneratedKeys(USER_ID_COLUMN); + + // situation1: insert columns are empty + List insertColumns = new ArrayList<>(); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(insertColumns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(true); + Assertions.assertIterableEquals(mockAllPkValuesFromColumn.entrySet(), insertExecutor.getPkValues().entrySet()); + + // situation2: insert columns contain all pk columns + insertColumns = new ArrayList<>(); + insertColumns.add(ID_COLUMN); + insertColumns.add(USER_ID_COLUMN); + insertColumns.add(USER_NAME_COLUMN); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(insertColumns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(false); + Assertions.assertIterableEquals(mockAllPkValuesFromColumn.entrySet(), insertExecutor.getPkValues().entrySet()); + + // situation3: insert columns contain partial pk columns + insertColumns = new ArrayList<>(); + insertColumns.add(ID_COLUMN); + insertColumns.add(USER_NAME_COLUMN); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(insertColumns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(false); + + Map> mockPkValuesFromColumn_ID = new HashMap<>(); + mockPkValuesFromColumn_ID.put(ID_COLUMN, Collections.singletonList(PK_VALUE_ID + 1)); + doReturn(mockPkValuesFromColumn_ID).when(insertExecutor).getPkValuesByColumn(); + + Map> expectPkValues = new HashMap<>(mockPkValuesFromColumn_ID); + expectPkValues.put(USER_ID_COLUMN, mockPkValuesAutoGenerated_USER_ID); + Assertions.assertIterableEquals(expectPkValues.entrySet(), insertExecutor.getPkValues().entrySet()); + + // situation4: insert columns are not empty and do not contain the pk column + insertColumns = new ArrayList<>(); + insertColumns.add(USER_NAME_COLUMN); + when(sqlInsertRecognizer.getInsertColumns()).thenReturn(insertColumns); + when(sqlInsertRecognizer.insertColumnsIsEmpty()).thenReturn(false); + + doReturn(new HashMap<>()).when(insertExecutor).getPkValuesByColumn(); + + expectPkValues = new HashMap<>(); + expectPkValues.put(ID_COLUMN, mockPkValuesAutoGenerated_ID); + expectPkValues.put(USER_ID_COLUMN, mockPkValuesAutoGenerated_USER_ID); + Assertions.assertIterableEquals(expectPkValues.entrySet(), insertExecutor.getPkValues().entrySet()); + } + + @Test + public void testContainsAnyPK() { + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + + Assertions.assertFalse(insertExecutor.containsAnyPk()); + + mockInsertColumns(); + doReturn(null).when(tableMeta).getPrimaryKeyOnlyName(); + Assertions.assertFalse(insertExecutor.containsAnyPk()); + + List pkColumns = new ArrayList<>(); + pkColumns.add(System.currentTimeMillis() + ""); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + Assertions.assertFalse(insertExecutor.containsAnyPk()); + + pkColumns = new ArrayList<>(); + pkColumns.add(ID_COLUMN); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + Assertions.assertTrue(insertExecutor.containsAnyPk()); + + pkColumns = new ArrayList<>(); + pkColumns.add(ID_COLUMN); + pkColumns.add(USER_ID_COLUMN); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + Assertions.assertTrue(insertExecutor.containsAnyPk()); + + pkColumns = new ArrayList<>(); + pkColumns.add(ID_COLUMN); + pkColumns.add(System.currentTimeMillis() + ""); + doReturn(pkColumns).when(tableMeta).getPrimaryKeyOnlyName(); + Assertions.assertTrue(insertExecutor.containsAnyPk()); + } + private void mockParametersPkWithDefault() { Map> parameters = new HashMap<>(4); ArrayList arrayList0 = new ArrayList<>();