mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Reject overlapping declared and generated key columns in SimpleJdbcInsert
When a column was declared via usingColumns() and also listed in usingGeneratedKeyColumns(), TableMetaDataContext.reconcileColumnsToUse accepted the declared list as-is: the generated key column was rendered into the INSERT statement and counted against the parameter values, even though the database is expected to generate its value. Such an overlap is a configuration error, so it is now rejected at compile time with an InvalidDataAccessApiUsageException naming the offending columns in their declared spelling, consistent with the existing validation in AbstractJdbcInsert.compile(). Matching is case-insensitive, mirroring the normalization used for auto-discovered columns; the auto-discovery path itself is unchanged and continues to exclude generated key columns silently. The tests cover the rejection, its message, a case-insensitive variant, and the untouched non-overlapping declared path. Closes gh-37014 Signed-off-by: junhyeong9812 <pickjog@gmail.com>
This commit is contained in:
+13
-3
@@ -205,13 +205,23 @@ public class TableMetaDataContext {
|
||||
if (generatedKeyNames.length > 0) {
|
||||
this.generatedKeyColumnsUsed = true;
|
||||
}
|
||||
if (!declaredColumns.isEmpty()) {
|
||||
return new ArrayList<>(declaredColumns);
|
||||
}
|
||||
Set<String> keys = CollectionUtils.newLinkedHashSet(generatedKeyNames.length);
|
||||
for (String key : generatedKeyNames) {
|
||||
keys.add(key.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
if (!declaredColumns.isEmpty()) {
|
||||
List<String> overlapping = new ArrayList<>();
|
||||
for (String column : declaredColumns) {
|
||||
if (keys.contains(column.toUpperCase(Locale.ROOT))) {
|
||||
overlapping.add(column);
|
||||
}
|
||||
}
|
||||
if (!overlapping.isEmpty()) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Declared columns " + overlapping + " must not overlap with generated key columns");
|
||||
}
|
||||
return new ArrayList<>(declaredColumns);
|
||||
}
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (TableParameterMetaData meta : obtainMetaDataProvider().getTableParameterMetaData()) {
|
||||
if (!keys.contains(meta.getParameterName().toUpperCase(Locale.ROOT))) {
|
||||
|
||||
+60
@@ -29,11 +29,13 @@ import javax.sql.DataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.jdbc.core.metadata.TableMetaDataContext;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -153,4 +155,62 @@ class TableMetaDataContextTests {
|
||||
verify(columnsResultSet).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlappingDeclaredAndGeneratedKeyColumnsAreRejected() throws Exception {
|
||||
initializeTwoColumnCustomersTable();
|
||||
context.setTableName("customers");
|
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> context.processMetaData(
|
||||
dataSource, List.of("id", "name"), new String[] { "id" }))
|
||||
.withMessage("Declared columns [id] must not overlap with generated key columns");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overlappingDeclaredAndGeneratedKeyColumnsAreRejectedRegardlessOfCase() throws Exception {
|
||||
initializeTwoColumnCustomersTable();
|
||||
context.setTableName("customers");
|
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> context.processMetaData(
|
||||
dataSource, List.of("ID", "name"), new String[] { "id" }))
|
||||
.withMessage("Declared columns [ID] must not overlap with generated key columns");
|
||||
}
|
||||
|
||||
@Test
|
||||
void declaredColumnsWithoutOverlapAreUsedAsIs() throws Exception {
|
||||
initializeTwoColumnCustomersTable();
|
||||
MapSqlParameterSource map = new MapSqlParameterSource();
|
||||
map.addValue("name", "Sven");
|
||||
String[] keyCols = new String[] { "id" };
|
||||
context.setTableName("customers");
|
||||
context.processMetaData(dataSource, List.of("name"), keyCols);
|
||||
List<Object> values = context.matchInParameterValuesWithInsertColumns(map);
|
||||
String insertString = context.createInsertString(keyCols);
|
||||
|
||||
assertThat(insertString).isEqualTo("INSERT INTO customers (name) VALUES(?)");
|
||||
assertThat(values).containsExactly("Sven");
|
||||
}
|
||||
|
||||
private void initializeTwoColumnCustomersTable() throws Exception {
|
||||
ResultSet metaDataResultSet = mock();
|
||||
given(metaDataResultSet.next()).willReturn(true, false);
|
||||
given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn("me");
|
||||
given(metaDataResultSet.getString("TABLE_NAME")).willReturn("customers");
|
||||
given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE");
|
||||
|
||||
ResultSet columnsResultSet = mock();
|
||||
given(columnsResultSet.next()).willReturn(true, true, false);
|
||||
given(columnsResultSet.getString("COLUMN_NAME")).willReturn("id", "name");
|
||||
given(columnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER, Types.VARCHAR);
|
||||
given(columnsResultSet.getBoolean("NULLABLE")).willReturn(false, true);
|
||||
|
||||
given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB");
|
||||
given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0");
|
||||
given(databaseMetaData.getUserName()).willReturn("me");
|
||||
given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true);
|
||||
given(databaseMetaData.getTables(null, null, "customers", null)).willReturn(metaDataResultSet);
|
||||
given(databaseMetaData.getColumns(null, "me", "customers", null)).willReturn(columnsResultSet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user