Skip to content

Commit 03272c7

Browse files
authored
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
1 parent e2b6180 commit 03272c7

File tree

9 files changed

+260
-0
lines changed

9 files changed

+260
-0
lines changed

doma-core/src/main/java/org/seasar/doma/internal/jdbc/command/EntityProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.List;
2828
import java.util.Map;
2929
import java.util.Set;
30+
import org.seasar.doma.jdbc.DuplicateColumnHandler;
3031
import org.seasar.doma.jdbc.JdbcMappingVisitor;
3132
import org.seasar.doma.jdbc.Naming;
3233
import org.seasar.doma.jdbc.ResultMappingException;
@@ -49,6 +50,8 @@ public class EntityProvider<ENTITY> extends AbstractObjectProvider<ENTITY> {
4950

5051
protected final UnknownColumnHandler unknownColumnHandler;
5152

53+
protected final DuplicateColumnHandler duplicateColumnHandler;
54+
5255
protected Map<Integer, EntityPropertyType<ENTITY, ?>> indexMap;
5356

5457
public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean resultMappingEnsured) {
@@ -58,6 +61,7 @@ public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean result
5861
this.resultMappingEnsured = resultMappingEnsured;
5962
this.jdbcMappingVisitor = query.getConfig().getDialect().getJdbcMappingVisitor();
6063
this.unknownColumnHandler = query.getConfig().getUnknownColumnHandler();
64+
this.duplicateColumnHandler = query.getConfig().getDuplicateColumnHandler();
6165
}
6266

6367
@Override
@@ -91,10 +95,14 @@ protected ENTITY build(ResultSet resultSet) throws SQLException {
9195
HashMap<String, EntityPropertyType<ENTITY, ?>> columnNameMap = createColumnNameMap(entityType);
9296
Set<EntityPropertyType<ENTITY, ?>> unmappedPropertySet =
9397
resultMappingEnsured ? new HashSet<>(columnNameMap.values()) : new HashSet<>();
98+
Set<String> seenColumnNames = new HashSet<>();
9499
int count = resultSetMeta.getColumnCount();
95100
for (int i = 1; i < count + 1; i++) {
96101
String columnName = resultSetMeta.getColumnLabel(i);
97102
String lowerCaseColumnName = columnName.toLowerCase();
103+
if (!seenColumnNames.add(lowerCaseColumnName)) {
104+
duplicateColumnHandler.handle(query, lowerCaseColumnName);
105+
}
98106
EntityPropertyType<ENTITY, ?> propertyType = columnNameMap.get(lowerCaseColumnName);
99107
if (propertyType == null) {
100108
if (ROWNUMBER_COLUMN_NAME.equals(lowerCaseColumnName)) {

doma-core/src/main/java/org/seasar/doma/jdbc/Config.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ default UnknownColumnHandler getUnknownColumnHandler() {
141141
return ConfigSupport.defaultUnknownColumnHandler;
142142
}
143143

144+
/**
145+
* Returns the duplicate column handler.
146+
*
147+
* @return the duplicate column handler
148+
*/
149+
default DuplicateColumnHandler getDuplicateColumnHandler() {
150+
return ConfigSupport.defaultDuplicateColumnHandler;
151+
}
152+
144153
/**
145154
* Returns the naming convention controller.
146155
*

doma-core/src/main/java/org/seasar/doma/jdbc/ConfigSupport.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public final class ConfigSupport {
4040
public static final UnknownColumnHandler defaultUnknownColumnHandler =
4141
new UnknownColumnHandler() {};
4242

43+
public static final DuplicateColumnHandler defaultDuplicateColumnHandler =
44+
new DuplicateColumnHandler() {};
45+
4346
public static final Naming defaultNaming = Naming.DEFAULT;
4447

4548
public static final MapKeyNaming defaultMapKeyNaming = new MapKeyNaming() {};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.jdbc;
17+
18+
import org.seasar.doma.message.Message;
19+
20+
/** Thrown to indicate that a column is duplicated in a result set. */
21+
public class DuplicateColumnException extends JdbcException {
22+
23+
private static final long serialVersionUID = 1L;
24+
25+
protected final String columnName;
26+
27+
protected final String rawSql;
28+
29+
protected final String formattedSql;
30+
31+
protected final String sqlFilePath;
32+
33+
public DuplicateColumnException(
34+
SqlLogType logType,
35+
String columnName,
36+
String rawSql,
37+
String formattedSql,
38+
String sqlFilePath) {
39+
super(Message.DOMA2237, columnName, sqlFilePath, choiceSql(logType, rawSql, formattedSql));
40+
this.columnName = columnName;
41+
this.rawSql = rawSql;
42+
this.formattedSql = formattedSql;
43+
this.sqlFilePath = sqlFilePath;
44+
}
45+
46+
/**
47+
* Returns the unknown column name.
48+
*
49+
* @return the unknown column name
50+
*/
51+
public String getColumnName() {
52+
return columnName;
53+
}
54+
55+
/**
56+
* Returns the raw SQL string.
57+
*
58+
* @return the raw SQL string
59+
*/
60+
public String getRawSql() {
61+
return rawSql;
62+
}
63+
64+
/**
65+
* Returns the formatted SQL string
66+
*
67+
* @return the formatted SQL or {@code null} if this exception is thrown in the batch process
68+
*/
69+
public String getFormattedSql() {
70+
return formattedSql;
71+
}
72+
73+
/**
74+
* Returns the SQL file path.
75+
*
76+
* @return the SQL file path or {@code null} if the SQL is auto generated
77+
*/
78+
public String getSqlFilePath() {
79+
return sqlFilePath;
80+
}
81+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.jdbc;
17+
18+
import org.seasar.doma.jdbc.query.Query;
19+
20+
/** A handler for the column that is duplicated in a result set. */
21+
public interface DuplicateColumnHandler {
22+
23+
/**
24+
* Handles the duplicate column.
25+
*
26+
* @param query the query
27+
* @param duplicateColumnName the name of the unknown column
28+
*/
29+
default void handle(Query query, String duplicateColumnName) {}
30+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.jdbc;
17+
18+
import org.seasar.doma.jdbc.query.Query;
19+
20+
/** A handler for the column that is duplicated in a result set. */
21+
public class ThrowingDuplicateColumnHandler implements DuplicateColumnHandler {
22+
23+
/**
24+
* Handles the duplicate column.
25+
*
26+
* @param query the query
27+
* @param duplicateColumnName the name of the duplicate column
28+
* @throws DuplicateColumnException if this handler does not allow the duplicate column
29+
*/
30+
@Override
31+
public void handle(Query query, String duplicateColumnName) {
32+
Sql<?> sql = query.getSql();
33+
throw new DuplicateColumnException(
34+
query.getConfig().getExceptionSqlLogType(),
35+
duplicateColumnName,
36+
sql.getRawSql(),
37+
sql.getFormattedSql(),
38+
sql.getSqlFilePath());
39+
}
40+
}

doma-core/src/main/java/org/seasar/doma/message/Message.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ public enum Message implements MessageResource {
274274
DOMA2235("The dialect \"{0}\" does not support auto-increment when inserting multiple rows."),
275275

276276
DOMA2236("The dialect \"{0}\" does not support multi-row insert statement."),
277+
DOMA2237(
278+
"Duplicate column name \"{0}\" found in ResultSetMetaData. Column names must be unique."
279+
+ "\nPATH=[{1}].\nSQL=[{2}]"),
277280

278281
// expression
279282
DOMA3001(

doma-core/src/test/java/org/seasar/doma/internal/jdbc/command/EntityProviderTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
*/
1616
package org.seasar.doma.internal.jdbc.command;
1717

18+
import static org.junit.jupiter.api.Assertions.assertThrows;
1819
import static org.junit.jupiter.api.Assertions.fail;
1920
import static org.seasar.doma.internal.util.AssertionUtil.assertEquals;
2021

2122
import example.entity.Emp;
2223
import example.entity._Emp;
2324
import java.lang.reflect.Method;
2425
import java.math.BigDecimal;
26+
import java.sql.SQLException;
2527
import java.util.Collections;
2628
import org.junit.jupiter.api.Test;
2729
import org.seasar.doma.FetchType;
@@ -31,10 +33,13 @@
3133
import org.seasar.doma.internal.jdbc.mock.MockResultSetMetaData;
3234
import org.seasar.doma.internal.jdbc.mock.RowData;
3335
import org.seasar.doma.jdbc.Config;
36+
import org.seasar.doma.jdbc.DuplicateColumnException;
37+
import org.seasar.doma.jdbc.DuplicateColumnHandler;
3438
import org.seasar.doma.jdbc.PreparedSql;
3539
import org.seasar.doma.jdbc.SelectOptions;
3640
import org.seasar.doma.jdbc.SqlKind;
3741
import org.seasar.doma.jdbc.SqlLogType;
42+
import org.seasar.doma.jdbc.ThrowingDuplicateColumnHandler;
3843
import org.seasar.doma.jdbc.UnknownColumnException;
3944
import org.seasar.doma.jdbc.UnknownColumnHandler;
4045
import org.seasar.doma.jdbc.entity.EntityType;
@@ -111,6 +116,46 @@ public void testGetEntity_EmptyUnknownColumnHandler() throws Exception {
111116
assertEquals(100, emp.getVersion());
112117
}
113118

119+
@Test
120+
public void testCreateIndexMap_WithDuplicateColumnName() throws SQLException {
121+
MockResultSetMetaData metaData = new MockResultSetMetaData();
122+
metaData.columns.add(new ColumnMetaData("id"));
123+
metaData.columns.add(new ColumnMetaData("name"));
124+
metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name
125+
metaData.columns.add(new ColumnMetaData("version"));
126+
MockResultSet resultSet = new MockResultSet(metaData);
127+
resultSet.rows.add(new RowData(1, "aaa", "bbb", 100));
128+
resultSet.next();
129+
130+
_Emp entityType = _Emp.getSingletonInternal();
131+
EntityProvider<Emp> provider =
132+
new EntityProvider<>(entityType, new MySelectQuery(new MockConfig()), false);
133+
134+
provider.createIndexMap(metaData, entityType);
135+
Emp emp = provider.get(resultSet);
136+
137+
assertEquals(1, emp.getId());
138+
assertEquals("bbb", emp.getName());
139+
assertEquals(100, emp.getVersion());
140+
}
141+
142+
@Test
143+
public void testCreateIndexMap_DuplicateColumnHandler() throws SQLException {
144+
MockResultSetMetaData metaData = new MockResultSetMetaData();
145+
metaData.columns.add(new ColumnMetaData("id"));
146+
metaData.columns.add(new ColumnMetaData("name"));
147+
metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name
148+
metaData.columns.add(new ColumnMetaData("version"));
149+
150+
_Emp entityType = _Emp.getSingletonInternal();
151+
EntityProvider<Emp> provider =
152+
new EntityProvider<>(
153+
entityType, new MySelectQuery(new SetDuplicateColumnHandlerConfig()), false);
154+
155+
assertThrows(
156+
DuplicateColumnException.class, () -> provider.createIndexMap(metaData, entityType));
157+
}
158+
114159
protected static class MySelectQuery implements SelectQuery {
115160

116161
private final Config config;
@@ -213,4 +258,11 @@ public UnknownColumnHandler getUnknownColumnHandler() {
213258
return new EmptyUnknownColumnHandler();
214259
}
215260
}
261+
262+
protected static class SetDuplicateColumnHandlerConfig extends MockConfig {
263+
@Override
264+
public DuplicateColumnHandler getDuplicateColumnHandler() {
265+
return new ThrowingDuplicateColumnHandler();
266+
}
267+
}
216268
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Doma Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.seasar.doma.jdbc;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
public class DuplicateColumnExceptionTest {
23+
24+
@Test
25+
public void test() {
26+
DuplicateColumnException e =
27+
new DuplicateColumnException(SqlLogType.FORMATTED, "aaa", "bbb", "ccc", "ddd");
28+
System.out.println(e.getMessage());
29+
assertEquals("aaa", e.getColumnName());
30+
assertEquals("bbb", e.getRawSql());
31+
assertEquals("ccc", e.getFormattedSql());
32+
assertEquals("ddd", e.getSqlFilePath());
33+
}
34+
}

0 commit comments

Comments
 (0)