From 03272c7e2c54c8f2bf336762fab699a6d7f58a21 Mon Sep 17 00:00:00 2001 From: okurashoichi Date: Wed, 15 Jan 2025 22:08:34 +0900 Subject: [PATCH] Add duplicate column detection in EntityProvider (#1267) * Add duplicate column detection in EntityProvider * Add DuplicateColumnHandler interface and its implementations to make DuplicateColumnException optional * Replace wildcard imports with explicit imports * current behavior set as the default implementation and DuplicateColumnException optional * Change duplicate column handler to use lower case column name * update test method name --- .../internal/jdbc/command/EntityProvider.java | 8 ++ .../java/org/seasar/doma/jdbc/Config.java | 9 +++ .../org/seasar/doma/jdbc/ConfigSupport.java | 3 + .../doma/jdbc/DuplicateColumnException.java | 81 +++++++++++++++++++ .../doma/jdbc/DuplicateColumnHandler.java | 30 +++++++ .../jdbc/ThrowingDuplicateColumnHandler.java | 40 +++++++++ .../java/org/seasar/doma/message/Message.java | 3 + .../jdbc/command/EntityProviderTest.java | 52 ++++++++++++ .../jdbc/DuplicateColumnExceptionTest.java | 34 ++++++++ 9 files changed, 260 insertions(+) create mode 100644 doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnException.java create mode 100644 doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnHandler.java create mode 100644 doma-core/src/main/java/org/seasar/doma/jdbc/ThrowingDuplicateColumnHandler.java create mode 100644 doma-core/src/test/java/org/seasar/doma/jdbc/DuplicateColumnExceptionTest.java diff --git a/doma-core/src/main/java/org/seasar/doma/internal/jdbc/command/EntityProvider.java b/doma-core/src/main/java/org/seasar/doma/internal/jdbc/command/EntityProvider.java index 3080bf348..4af621fe2 100644 --- a/doma-core/src/main/java/org/seasar/doma/internal/jdbc/command/EntityProvider.java +++ b/doma-core/src/main/java/org/seasar/doma/internal/jdbc/command/EntityProvider.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.seasar.doma.jdbc.DuplicateColumnHandler; import org.seasar.doma.jdbc.JdbcMappingVisitor; import org.seasar.doma.jdbc.Naming; import org.seasar.doma.jdbc.ResultMappingException; @@ -49,6 +50,8 @@ public class EntityProvider extends AbstractObjectProvider { protected final UnknownColumnHandler unknownColumnHandler; + protected final DuplicateColumnHandler duplicateColumnHandler; + protected Map> indexMap; public EntityProvider(EntityType entityType, Query query, boolean resultMappingEnsured) { @@ -58,6 +61,7 @@ public EntityProvider(EntityType entityType, Query query, boolean result this.resultMappingEnsured = resultMappingEnsured; this.jdbcMappingVisitor = query.getConfig().getDialect().getJdbcMappingVisitor(); this.unknownColumnHandler = query.getConfig().getUnknownColumnHandler(); + this.duplicateColumnHandler = query.getConfig().getDuplicateColumnHandler(); } @Override @@ -91,10 +95,14 @@ protected ENTITY build(ResultSet resultSet) throws SQLException { HashMap> columnNameMap = createColumnNameMap(entityType); Set> unmappedPropertySet = resultMappingEnsured ? new HashSet<>(columnNameMap.values()) : new HashSet<>(); + Set seenColumnNames = new HashSet<>(); int count = resultSetMeta.getColumnCount(); for (int i = 1; i < count + 1; i++) { String columnName = resultSetMeta.getColumnLabel(i); String lowerCaseColumnName = columnName.toLowerCase(); + if (!seenColumnNames.add(lowerCaseColumnName)) { + duplicateColumnHandler.handle(query, lowerCaseColumnName); + } EntityPropertyType propertyType = columnNameMap.get(lowerCaseColumnName); if (propertyType == null) { if (ROWNUMBER_COLUMN_NAME.equals(lowerCaseColumnName)) { diff --git a/doma-core/src/main/java/org/seasar/doma/jdbc/Config.java b/doma-core/src/main/java/org/seasar/doma/jdbc/Config.java index ab041f978..52172e88b 100644 --- a/doma-core/src/main/java/org/seasar/doma/jdbc/Config.java +++ b/doma-core/src/main/java/org/seasar/doma/jdbc/Config.java @@ -141,6 +141,15 @@ default UnknownColumnHandler getUnknownColumnHandler() { return ConfigSupport.defaultUnknownColumnHandler; } + /** + * Returns the duplicate column handler. + * + * @return the duplicate column handler + */ + default DuplicateColumnHandler getDuplicateColumnHandler() { + return ConfigSupport.defaultDuplicateColumnHandler; + } + /** * Returns the naming convention controller. * diff --git a/doma-core/src/main/java/org/seasar/doma/jdbc/ConfigSupport.java b/doma-core/src/main/java/org/seasar/doma/jdbc/ConfigSupport.java index 17723fcd2..282492ea9 100644 --- a/doma-core/src/main/java/org/seasar/doma/jdbc/ConfigSupport.java +++ b/doma-core/src/main/java/org/seasar/doma/jdbc/ConfigSupport.java @@ -40,6 +40,9 @@ public final class ConfigSupport { public static final UnknownColumnHandler defaultUnknownColumnHandler = new UnknownColumnHandler() {}; + public static final DuplicateColumnHandler defaultDuplicateColumnHandler = + new DuplicateColumnHandler() {}; + public static final Naming defaultNaming = Naming.DEFAULT; public static final MapKeyNaming defaultMapKeyNaming = new MapKeyNaming() {}; diff --git a/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnException.java b/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnException.java new file mode 100644 index 000000000..edbdc7bee --- /dev/null +++ b/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnException.java @@ -0,0 +1,81 @@ +/* + * Copyright Doma Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.seasar.doma.jdbc; + +import org.seasar.doma.message.Message; + +/** Thrown to indicate that a column is duplicated in a result set. */ +public class DuplicateColumnException extends JdbcException { + + private static final long serialVersionUID = 1L; + + protected final String columnName; + + protected final String rawSql; + + protected final String formattedSql; + + protected final String sqlFilePath; + + public DuplicateColumnException( + SqlLogType logType, + String columnName, + String rawSql, + String formattedSql, + String sqlFilePath) { + super(Message.DOMA2237, columnName, sqlFilePath, choiceSql(logType, rawSql, formattedSql)); + this.columnName = columnName; + this.rawSql = rawSql; + this.formattedSql = formattedSql; + this.sqlFilePath = sqlFilePath; + } + + /** + * Returns the unknown column name. + * + * @return the unknown column name + */ + public String getColumnName() { + return columnName; + } + + /** + * Returns the raw SQL string. + * + * @return the raw SQL string + */ + public String getRawSql() { + return rawSql; + } + + /** + * Returns the formatted SQL string + * + * @return the formatted SQL or {@code null} if this exception is thrown in the batch process + */ + public String getFormattedSql() { + return formattedSql; + } + + /** + * Returns the SQL file path. + * + * @return the SQL file path or {@code null} if the SQL is auto generated + */ + public String getSqlFilePath() { + return sqlFilePath; + } +} diff --git a/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnHandler.java b/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnHandler.java new file mode 100644 index 000000000..0caf1d606 --- /dev/null +++ b/doma-core/src/main/java/org/seasar/doma/jdbc/DuplicateColumnHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright Doma Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.seasar.doma.jdbc; + +import org.seasar.doma.jdbc.query.Query; + +/** A handler for the column that is duplicated in a result set. */ +public interface DuplicateColumnHandler { + + /** + * Handles the duplicate column. + * + * @param query the query + * @param duplicateColumnName the name of the unknown column + */ + default void handle(Query query, String duplicateColumnName) {} +} diff --git a/doma-core/src/main/java/org/seasar/doma/jdbc/ThrowingDuplicateColumnHandler.java b/doma-core/src/main/java/org/seasar/doma/jdbc/ThrowingDuplicateColumnHandler.java new file mode 100644 index 000000000..682fc3c7d --- /dev/null +++ b/doma-core/src/main/java/org/seasar/doma/jdbc/ThrowingDuplicateColumnHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright Doma Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.seasar.doma.jdbc; + +import org.seasar.doma.jdbc.query.Query; + +/** A handler for the column that is duplicated in a result set. */ +public class ThrowingDuplicateColumnHandler implements DuplicateColumnHandler { + + /** + * Handles the duplicate column. + * + * @param query the query + * @param duplicateColumnName the name of the duplicate column + * @throws DuplicateColumnException if this handler does not allow the duplicate column + */ + @Override + public void handle(Query query, String duplicateColumnName) { + Sql sql = query.getSql(); + throw new DuplicateColumnException( + query.getConfig().getExceptionSqlLogType(), + duplicateColumnName, + sql.getRawSql(), + sql.getFormattedSql(), + sql.getSqlFilePath()); + } +} diff --git a/doma-core/src/main/java/org/seasar/doma/message/Message.java b/doma-core/src/main/java/org/seasar/doma/message/Message.java index a8f728e14..674c8ab7d 100644 --- a/doma-core/src/main/java/org/seasar/doma/message/Message.java +++ b/doma-core/src/main/java/org/seasar/doma/message/Message.java @@ -274,6 +274,9 @@ public enum Message implements MessageResource { DOMA2235("The dialect \"{0}\" does not support auto-increment when inserting multiple rows."), DOMA2236("The dialect \"{0}\" does not support multi-row insert statement."), + DOMA2237( + "Duplicate column name \"{0}\" found in ResultSetMetaData. Column names must be unique." + + "\nPATH=[{1}].\nSQL=[{2}]"), // expression DOMA3001( diff --git a/doma-core/src/test/java/org/seasar/doma/internal/jdbc/command/EntityProviderTest.java b/doma-core/src/test/java/org/seasar/doma/internal/jdbc/command/EntityProviderTest.java index 01f0f7b3f..696ebf1b8 100644 --- a/doma-core/src/test/java/org/seasar/doma/internal/jdbc/command/EntityProviderTest.java +++ b/doma-core/src/test/java/org/seasar/doma/internal/jdbc/command/EntityProviderTest.java @@ -15,6 +15,7 @@ */ package org.seasar.doma.internal.jdbc.command; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.seasar.doma.internal.util.AssertionUtil.assertEquals; @@ -22,6 +23,7 @@ import example.entity._Emp; import java.lang.reflect.Method; import java.math.BigDecimal; +import java.sql.SQLException; import java.util.Collections; import org.junit.jupiter.api.Test; import org.seasar.doma.FetchType; @@ -31,10 +33,13 @@ import org.seasar.doma.internal.jdbc.mock.MockResultSetMetaData; import org.seasar.doma.internal.jdbc.mock.RowData; import org.seasar.doma.jdbc.Config; +import org.seasar.doma.jdbc.DuplicateColumnException; +import org.seasar.doma.jdbc.DuplicateColumnHandler; import org.seasar.doma.jdbc.PreparedSql; import org.seasar.doma.jdbc.SelectOptions; import org.seasar.doma.jdbc.SqlKind; import org.seasar.doma.jdbc.SqlLogType; +import org.seasar.doma.jdbc.ThrowingDuplicateColumnHandler; import org.seasar.doma.jdbc.UnknownColumnException; import org.seasar.doma.jdbc.UnknownColumnHandler; import org.seasar.doma.jdbc.entity.EntityType; @@ -111,6 +116,46 @@ public void testGetEntity_EmptyUnknownColumnHandler() throws Exception { assertEquals(100, emp.getVersion()); } + @Test + public void testCreateIndexMap_WithDuplicateColumnName() throws SQLException { + MockResultSetMetaData metaData = new MockResultSetMetaData(); + metaData.columns.add(new ColumnMetaData("id")); + metaData.columns.add(new ColumnMetaData("name")); + metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name + metaData.columns.add(new ColumnMetaData("version")); + MockResultSet resultSet = new MockResultSet(metaData); + resultSet.rows.add(new RowData(1, "aaa", "bbb", 100)); + resultSet.next(); + + _Emp entityType = _Emp.getSingletonInternal(); + EntityProvider provider = + new EntityProvider<>(entityType, new MySelectQuery(new MockConfig()), false); + + provider.createIndexMap(metaData, entityType); + Emp emp = provider.get(resultSet); + + assertEquals(1, emp.getId()); + assertEquals("bbb", emp.getName()); + assertEquals(100, emp.getVersion()); + } + + @Test + public void testCreateIndexMap_DuplicateColumnHandler() throws SQLException { + MockResultSetMetaData metaData = new MockResultSetMetaData(); + metaData.columns.add(new ColumnMetaData("id")); + metaData.columns.add(new ColumnMetaData("name")); + metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name + metaData.columns.add(new ColumnMetaData("version")); + + _Emp entityType = _Emp.getSingletonInternal(); + EntityProvider provider = + new EntityProvider<>( + entityType, new MySelectQuery(new SetDuplicateColumnHandlerConfig()), false); + + assertThrows( + DuplicateColumnException.class, () -> provider.createIndexMap(metaData, entityType)); + } + protected static class MySelectQuery implements SelectQuery { private final Config config; @@ -213,4 +258,11 @@ public UnknownColumnHandler getUnknownColumnHandler() { return new EmptyUnknownColumnHandler(); } } + + protected static class SetDuplicateColumnHandlerConfig extends MockConfig { + @Override + public DuplicateColumnHandler getDuplicateColumnHandler() { + return new ThrowingDuplicateColumnHandler(); + } + } } diff --git a/doma-core/src/test/java/org/seasar/doma/jdbc/DuplicateColumnExceptionTest.java b/doma-core/src/test/java/org/seasar/doma/jdbc/DuplicateColumnExceptionTest.java new file mode 100644 index 000000000..26a5092a1 --- /dev/null +++ b/doma-core/src/test/java/org/seasar/doma/jdbc/DuplicateColumnExceptionTest.java @@ -0,0 +1,34 @@ +/* + * Copyright Doma Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.seasar.doma.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class DuplicateColumnExceptionTest { + + @Test + public void test() { + DuplicateColumnException e = + new DuplicateColumnException(SqlLogType.FORMATTED, "aaa", "bbb", "ccc", "ddd"); + System.out.println(e.getMessage()); + assertEquals("aaa", e.getColumnName()); + assertEquals("bbb", e.getRawSql()); + assertEquals("ccc", e.getFormattedSql()); + assertEquals("ddd", e.getSqlFilePath()); + } +}