Skip to content

Commit 6c332a2

Browse files
authored
Support creating aggregate objects using arbitrary SQL (#1274)
2 parents 34aecb0 + 0ed7d98 commit 6c332a2

File tree

139 files changed

+7161
-74
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+7161
-74
lines changed

doma-core/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
exports org.seasar.doma.internal.expr.node;
5858
exports org.seasar.doma.internal.jdbc.sql.node;
5959
exports org.seasar.doma.internal.jdbc.util;
60+
exports org.seasar.doma.jdbc.aggregate;
6061

6162
// Requires
6263
requires transitive java.sql;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Indicates a strategy for aggregating database records.
25+
*
26+
* <p>The element annotated with {@linkplain AggregateStrategy} must be an interface.
27+
*
28+
* <pre>
29+
* &#064;AggregateStrategy(root = Department.class, tableAlias = "d")
30+
* interface DepartmentStrategy {
31+
*
32+
* &#064;AssociationLinker(propertyPath = "employeeList", tableAlias = "e")
33+
* BiFunction&lt;Department, Employee, Department&gt; employeeList = (d, e) -> {
34+
* d.getEmployeeList().add(e);
35+
* e.setDepartment(d);
36+
* return d;
37+
* };
38+
*
39+
* &#064;AssociationLinker(propertyPath = "employeeList.address", tableAlias = "a")
40+
* BiFunction&lt;Employee, Address, Employee&gt; employeeListAddress = (e, a) -> {
41+
* e.setAddress(a);
42+
* return e;
43+
* };
44+
* }
45+
* </pre>
46+
*/
47+
@Target({ElementType.TYPE})
48+
@Retention(RetentionPolicy.RUNTIME)
49+
public @interface AggregateStrategy {
50+
51+
/**
52+
* Specifies the root entity class associated with the aggregation strategy.
53+
*
54+
* @return the root class
55+
*/
56+
Class<?> root();
57+
58+
/**
59+
* Specifies the root table alias.
60+
*
61+
* @return the alias name for a root table
62+
*/
63+
String tableAlias();
64+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Indicates an association between entities.
25+
*
26+
* <pre>
27+
* &#064;Entity
28+
* class Department {
29+
* &#064;Association
30+
* List&lt;Employee&gt; employeeList;
31+
* }
32+
*
33+
* &#064;Entity
34+
* class Employee {
35+
* &#064;Association
36+
* Department department;
37+
* }
38+
* </pre>
39+
*
40+
* <p>This annotation is applied to fields that represent a relationship between entities.
41+
*/
42+
@Target(ElementType.FIELD)
43+
@Retention(RetentionPolicy.RUNTIME)
44+
@EntityField
45+
public @interface Association {}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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;
17+
18+
import java.lang.annotation.Retention;
19+
import java.lang.annotation.RetentionPolicy;
20+
import java.lang.annotation.Target;
21+
import java.util.function.BiFunction;
22+
23+
/**
24+
* Indicates the execution of associations between entities.
25+
*
26+
* <p>{@linkplain AssociationLinker} can be annotated on a field of {@link BiFunction}.
27+
*
28+
* <p>For the {@linkplain BiFunction}, specify the entity class represented by the {@link
29+
* #propertyPath()} as the second type parameter. For the first and third type parameters, specify
30+
* the source entity class associated with the entity class given in the second type parameter.
31+
*
32+
* <pre>
33+
* &#064;AggregateStrategy(root = Department.class, tableAlias = "d")
34+
* interface DepartmentStrategy {
35+
*
36+
* &#064;AssociationLinker(propertyPath = "employeeList", tableAlias = "e")
37+
* BiFunction&lt;Department, Employee, Department&gt; employeeList = (d, e) -> {
38+
* d.getEmployeeList().add(e);
39+
* e.setDepartment(d);
40+
* return d;
41+
* };
42+
*
43+
* &#064;AssociationLinker(propertyPath = "employeeList.address", tableAlias = "a")
44+
* BiFunction&lt;Employee, Address, Employee&gt; employeeListAddress = (e, a) -> {
45+
* e.setAddress(a);
46+
* return e;
47+
* };
48+
* }
49+
* </pre>
50+
*/
51+
@Target(java.lang.annotation.ElementType.FIELD)
52+
@Retention(RetentionPolicy.RUNTIME)
53+
public @interface AssociationLinker {
54+
/**
55+
* Specifies the property path from the root entity using dot notation.
56+
*
57+
* <p>For example, suppose the root {@code Department} class has a property {@code List<Employee>
58+
* employeeList}, and {@code Employee} has a property {@code Address address}. In this case, the
59+
* property path representing {@code address} would be {@code employeeList.address}.
60+
*
61+
* @return the property path as a string
62+
*/
63+
String propertyPath();
64+
65+
/**
66+
* Specifies the table alias for the entity class represented by the {@link #propertyPath()}.
67+
*
68+
* <p>For example, consider the following SQL:
69+
*
70+
* <pre>
71+
* SELECT * FROM DEPARTMENT d INNER JOIN EMPLOYEE e ON d.DEPARTMENT_ID = e.DEPARTMENT_ID
72+
* </pre>
73+
*
74+
* In this case, the {@code Employee} class corresponds to the {@code EMPLOYEE} table, and its
75+
* table alias is {@code e}.
76+
*
77+
* <p>The table alias concatenated with an underscore serves as the prefix for column aliases.
78+
*
79+
* @return the table alias as a string
80+
*/
81+
String tableAlias();
82+
}

doma-core/src/main/java/org/seasar/doma/Select.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,13 @@
163163
* @return the output format of SQL logs.
164164
*/
165165
SqlLogType sqlLog() default SqlLogType.FORMATTED;
166+
167+
/**
168+
* Defines how an aggregate is constructed.
169+
*
170+
* <p>Specifies the interface annotated with {@link AggregateStrategy}.
171+
*
172+
* @return the class representing the aggregation strategy, or {@code Void.class} if not specified
173+
*/
174+
Class<?> aggregateStrategy() default Void.class;
166175
}

doma-core/src/main/java/org/seasar/doma/internal/ClassNames.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ public static ClassName newEntityMetamodelClassNameBuilder(
6464
return new MetamodelClassNameBuilder(entityClassName, metamodelPrefix, metamodelSuffix).build();
6565
}
6666

67+
public static ClassName newAggregateStrategyTypeClassName(
68+
CharSequence aggregateStrategyClassName) {
69+
assertNotNull(aggregateStrategyClassName);
70+
return new ClassNameBuilder(aggregateStrategyClassName).build();
71+
}
72+
6773
private static class ClassNameBuilder {
6874

6975
final String binaryName;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.internal.jdbc.command;
17+
18+
import java.util.Map;
19+
import java.util.Objects;
20+
import java.util.TreeMap;
21+
22+
class ColumnNameMapFormatter {
23+
static String format(Map<String, MappingSupport.PropType> columnNameMap) {
24+
Objects.requireNonNull(columnNameMap);
25+
26+
StringBuilder buf = new StringBuilder();
27+
buf.append("------------------------------------------------------\n");
28+
buf.append("Lowercase Column Name -> Property Name (Entity Name)\n");
29+
buf.append("------------------------------------------------------\n");
30+
TreeMap<String, MappingSupport.PropType> sortedMap = new TreeMap<>(columnNameMap);
31+
for (Map.Entry<String, MappingSupport.PropType> entry : sortedMap.entrySet()) {
32+
String columnName = entry.getKey();
33+
MappingSupport.PropType propType = entry.getValue();
34+
buf.append(columnName);
35+
buf.append(" -> ");
36+
buf.append(propType.name());
37+
buf.append(" (");
38+
buf.append(propType.entityType().getName());
39+
buf.append(")\n");
40+
}
41+
buf.append("------------------------------------------------------");
42+
return buf.toString();
43+
}
44+
}

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

Lines changed: 35 additions & 1 deletion
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 java.util.stream.Collectors;
3031
import org.seasar.doma.jdbc.DuplicateColumnHandler;
3132
import org.seasar.doma.jdbc.JdbcMappingVisitor;
3233
import org.seasar.doma.jdbc.Naming;
@@ -52,6 +53,8 @@ public class EntityProvider<ENTITY> extends AbstractObjectProvider<ENTITY> {
5253

5354
protected final DuplicateColumnHandler duplicateColumnHandler;
5455

56+
private final MappingSupport mappingSupport;
57+
5558
protected Map<Integer, EntityPropertyType<ENTITY, ?>> indexMap;
5659

5760
public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean resultMappingEnsured) {
@@ -62,6 +65,9 @@ public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean result
6265
this.jdbcMappingVisitor = query.getConfig().getDialect().getJdbcMappingVisitor();
6366
this.unknownColumnHandler = query.getConfig().getUnknownColumnHandler();
6467
this.duplicateColumnHandler = query.getConfig().getDuplicateColumnHandler();
68+
this.mappingSupport =
69+
new MappingSupport(
70+
entityType, query, resultMappingEnsured, unknownColumnHandler, duplicateColumnHandler);
6571
}
6672

6773
@Override
@@ -72,7 +78,7 @@ public ENTITY get(ResultSet resultSet) throws SQLException {
7278
protected ENTITY build(ResultSet resultSet) throws SQLException {
7379
assertNotNull(resultSet);
7480
if (indexMap == null) {
75-
indexMap = createIndexMap(resultSet.getMetaData(), entityType);
81+
indexMap = createIndexMap(resultSet.getMetaData());
7682
}
7783
Map<String, Property<ENTITY, ?>> states = new HashMap<>(indexMap.size());
7884
for (Map.Entry<Integer, EntityPropertyType<ENTITY, ?>> entry : indexMap.entrySet()) {
@@ -89,6 +95,8 @@ protected ENTITY build(ResultSet resultSet) throws SQLException {
8995
return entity;
9096
}
9197

98+
@SuppressWarnings("removal")
99+
@Deprecated(forRemoval = true)
92100
protected HashMap<Integer, EntityPropertyType<ENTITY, ?>> createIndexMap(
93101
ResultSetMetaData resultSetMeta, EntityType<ENTITY> entityType) throws SQLException {
94102
HashMap<Integer, EntityPropertyType<ENTITY, ?>> indexMap = new HashMap<>();
@@ -120,6 +128,19 @@ protected ENTITY build(ResultSet resultSet) throws SQLException {
120128
return indexMap;
121129
}
122130

131+
@SuppressWarnings("unchecked")
132+
private Map<Integer, EntityPropertyType<ENTITY, ?>> createIndexMap(
133+
ResultSetMetaData resultSetMeta) throws SQLException {
134+
Map<String, MappingSupport.PropType> columnNameMap = createColumnNameMap();
135+
return indexMap =
136+
mappingSupport.createIndexMap(resultSetMeta, columnNameMap).entrySet().stream()
137+
.collect(
138+
Collectors.toMap(
139+
Map.Entry::getKey,
140+
e -> (EntityPropertyType<ENTITY, ?>) e.getValue().propertyType()));
141+
}
142+
143+
@Deprecated
123144
protected HashMap<String, EntityPropertyType<ENTITY, ?>> createColumnNameMap(
124145
EntityType<ENTITY> entityType) {
125146
Naming naming = query.getConfig().getNaming();
@@ -132,6 +153,19 @@ protected ENTITY build(ResultSet resultSet) throws SQLException {
132153
return result;
133154
}
134155

156+
private HashMap<String, MappingSupport.PropType> createColumnNameMap() {
157+
Naming naming = query.getConfig().getNaming();
158+
List<EntityPropertyType<ENTITY, ?>> propertyTypes = entityType.getEntityPropertyTypes();
159+
HashMap<String, MappingSupport.PropType> result = new HashMap<>(propertyTypes.size());
160+
for (EntityPropertyType<ENTITY, ?> propertyType : propertyTypes) {
161+
String columnName = propertyType.getColumnName(naming::apply);
162+
result.put(
163+
columnName.toLowerCase(), new MappingSupport.PropType(entityType, propertyType, ""));
164+
}
165+
return result;
166+
}
167+
168+
@Deprecated(forRemoval = true)
135169
protected void throwResultMappingException(
136170
Set<EntityPropertyType<ENTITY, ?>> unmappedPropertySet) {
137171
Naming naming = query.getConfig().getNaming();

0 commit comments

Comments
 (0)