Skip to content

Commit e26b6c2

Browse files
authored
Fix self-referential association duplication issue (#1278)
2 parents 6c332a2 + 8f1add4 commit e26b6c2

File tree

7 files changed

+98
-13
lines changed

7 files changed

+98
-13
lines changed

doma-core/src/main/java/org/seasar/doma/jdbc/criteria/command/AssociateCommand.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
import static java.util.stream.Collectors.toList;
1919

20+
import java.util.HashMap;
2021
import java.util.LinkedHashMap;
22+
import java.util.LinkedHashSet;
2123
import java.util.List;
2224
import java.util.Map;
2325
import java.util.Objects;
26+
import java.util.Set;
2427
import java.util.function.BiFunction;
2528
import org.seasar.doma.internal.util.Combinations;
2629
import org.seasar.doma.internal.util.Pair;
@@ -47,23 +50,26 @@ public AssociateCommand(
4750
@Override
4851
@SuppressWarnings("unchecked")
4952
public List<ENTITY> execute() {
50-
Map<EntityKey, Object> cache = new LinkedHashMap<>();
53+
Set<CacheKey> rootEntityKeys = new LinkedHashSet<>();
54+
Map<CacheKey, Object> cache = new HashMap<>();
5155
Combinations<EntityKey> combinations = new Combinations<>();
5256
SelectCommand<List<EntityPool>> command =
5357
new SelectCommand<>(
54-
query, new EntityPoolIterationHandler(context.getProjectionEntityMetamodels()));
58+
query,
59+
new EntityPoolIterationHandler(
60+
entityMetamodel, rootEntityKeys, context.getProjectionEntityMetamodels()));
5561
List<EntityPool> entityPools = command.execute();
5662
for (EntityPool entityPool : entityPools) {
5763
Map<EntityMetamodel<?>, Pair<EntityKey, Object>> associationCandidate = new LinkedHashMap<>();
5864
for (Map.Entry<EntityKey, EntityData> e : entityPool.entrySet()) {
5965
EntityKey key = e.getKey();
6066
EntityData data = e.getValue();
67+
CacheKey cacheKey = CacheKey.of(key);
6168
Object entity =
6269
cache.computeIfAbsent(
63-
key,
70+
cacheKey,
6471
k -> {
65-
EntityMetamodel<?> entityMetamodel = k.getEntityMetamodel();
66-
EntityType<Object> entityType = (EntityType<Object>) entityMetamodel.asType();
72+
EntityType<Object> entityType = (EntityType<Object>) k.entityType();
6773
Object newEntity = entityType.newEntity(data.getStates());
6874
if (!entityType.isImmutable()) {
6975
entityType.saveCurrentStates(newEntity);
@@ -75,14 +81,11 @@ public List<ENTITY> execute() {
7581
associate(cache, combinations, associationCandidate);
7682
}
7783
return (List<ENTITY>)
78-
cache.entrySet().stream()
79-
.filter(e -> e.getKey().getEntityMetamodel() == entityMetamodel)
80-
.map(Map.Entry::getValue)
81-
.collect(toList());
84+
rootEntityKeys.stream().map(cache::get).filter(Objects::nonNull).collect(toList());
8285
}
8386

8487
private void associate(
85-
Map<EntityKey, Object> cache,
88+
Map<CacheKey, Object> cache,
8689
Combinations<EntityKey> combinations,
8790
Map<EntityMetamodel<?>, Pair<EntityKey, Object>> associationCandidate) {
8891
for (Map.Entry<Pair<EntityMetamodel<?>, EntityMetamodel<?>>, BiFunction<Object, Object, Object>>
@@ -100,7 +103,7 @@ private void associate(
100103
}
101104
Object newEntity = associator.apply(keyAndEntity1.snd, keyAndEntity2.snd);
102105
if (newEntity != null) {
103-
cache.replace(keyAndEntity1.fst, newEntity);
106+
cache.replace(CacheKey.of(keyAndEntity1.fst), newEntity);
104107
associationCandidate.replace(metamodelPair.fst, new Pair<>(keyAndEntity1.fst, newEntity));
105108
}
106109
combinations.add(keyPair);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.criteria.command;
17+
18+
import java.util.List;
19+
import org.seasar.doma.jdbc.entity.EntityType;
20+
21+
public record CacheKey(EntityType<?> entityType, List<?> items) {
22+
static CacheKey of(EntityKey key) {
23+
return new CacheKey(key.getEntityMetamodel().asType(), key.getItems());
24+
}
25+
}

doma-core/src/main/java/org/seasar/doma/jdbc/criteria/command/EntityKey.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public EntityMetamodel<?> getEntityMetamodel() {
3434
return entityMetamodel;
3535
}
3636

37+
public List<?> getItems() {
38+
return items;
39+
}
40+
3741
@Override
3842
public boolean equals(Object o) {
3943
if (this == o) return true;

doma-core/src/main/java/org/seasar/doma/jdbc/criteria/command/EntityPoolIterationHandler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919
import java.util.Map;
2020
import java.util.Objects;
21+
import java.util.Set;
2122
import org.seasar.doma.internal.jdbc.command.AbstractIterationHandler;
2223
import org.seasar.doma.internal.jdbc.command.ResultListCallback;
2324
import org.seasar.doma.jdbc.ObjectProvider;
@@ -27,16 +28,23 @@
2728

2829
public class EntityPoolIterationHandler
2930
extends AbstractIterationHandler<EntityPool, List<EntityPool>> {
31+
private final EntityMetamodel<?> rootEntityMetamodel;
32+
private final Set<CacheKey> rootEntityKeys;
3033
private final Map<EntityMetamodel<?>, List<PropertyMetamodel<?>>> projectionEntityMetamodels;
3134

3235
public EntityPoolIterationHandler(
36+
EntityMetamodel<?> rootEntityMetamodel,
37+
Set<CacheKey> rootEntityKeys,
3338
Map<EntityMetamodel<?>, List<PropertyMetamodel<?>>> projectionEntityMetamodels) {
3439
super(new ResultListCallback<>());
40+
this.rootEntityMetamodel = Objects.requireNonNull(rootEntityMetamodel);
41+
this.rootEntityKeys = Objects.requireNonNull(rootEntityKeys);
3542
this.projectionEntityMetamodels = Objects.requireNonNull(projectionEntityMetamodels);
3643
}
3744

3845
@Override
3946
protected ObjectProvider<EntityPool> createObjectProvider(SelectQuery query) {
40-
return new EntityPoolProvider(projectionEntityMetamodels, query);
47+
return new EntityPoolProvider(
48+
rootEntityMetamodel, rootEntityKeys, projectionEntityMetamodels, query);
4149
}
4250
}

doma-core/src/main/java/org/seasar/doma/jdbc/criteria/command/EntityPoolProvider.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Objects;
27+
import java.util.Set;
2728
import java.util.stream.Collectors;
2829
import org.seasar.doma.internal.jdbc.command.FetchSupport;
2930
import org.seasar.doma.jdbc.ObjectProvider;
@@ -35,11 +36,18 @@
3536
import org.seasar.doma.jdbc.query.Query;
3637

3738
public class EntityPoolProvider implements ObjectProvider<EntityPool> {
39+
private final EntityMetamodel<?> rootEntityMetamodel;
40+
private final Set<CacheKey> rootEntityKeys;
3841
private final Map<EntityMetamodel<?>, List<PropertyMetamodel<?>>> projectionEntityMetamodels;
3942
private final FetchSupport fetchSupport;
4043

4144
public EntityPoolProvider(
42-
Map<EntityMetamodel<?>, List<PropertyMetamodel<?>>> projectionEntityMetamodels, Query query) {
45+
EntityMetamodel<?> rootEntityMetamodel,
46+
Set<CacheKey> rootEntityKeys,
47+
Map<EntityMetamodel<?>, List<PropertyMetamodel<?>>> projectionEntityMetamodels,
48+
Query query) {
49+
this.rootEntityMetamodel = Objects.requireNonNull(rootEntityMetamodel);
50+
this.rootEntityKeys = Objects.requireNonNull(rootEntityKeys);
4351
this.projectionEntityMetamodels = Objects.requireNonNull(projectionEntityMetamodels);
4452
Objects.requireNonNull(query);
4553
this.fetchSupport = new FetchSupport(query);
@@ -82,6 +90,9 @@ public EntityPool get(ResultSet resultSet) throws SQLException {
8290
props.stream().collect(Collectors.toMap(p -> p.propType.getName(), p -> p.prop));
8391
EntityData data = new EntityData(states);
8492
entityPool.put(key, data);
93+
if (key.getEntityMetamodel() == rootEntityMetamodel) {
94+
rootEntityKeys.add(CacheKey.of(key));
95+
}
8596
}
8697
return entityPool;
8798
}

doma-core/src/main/java/org/seasar/doma/jdbc/criteria/metamodel/EntityTypeProxy.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,17 @@ public void postUpdate(ENTITY entity, PostUpdateContext<ENTITY> context) {
182182
public void postDelete(ENTITY entity, PostDeleteContext<ENTITY> context) {
183183
entityType.postDelete(entity, context);
184184
}
185+
186+
@Override
187+
public boolean equals(Object o) {
188+
if (o == null || getClass() != o.getClass()) return false;
189+
EntityTypeProxy<?> that = (EntityTypeProxy<?>) o;
190+
return Objects.equals(entityType, that.entityType)
191+
&& Objects.equals(qualifiedTableName, that.qualifiedTableName);
192+
}
193+
194+
@Override
195+
public int hashCode() {
196+
return Objects.hash(entityType, qualifiedTableName);
197+
}
185198
}

integration-test-java/src/test/java/org/seasar/doma/it/criteria/QueryDslEntitySelectTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
2121
import static org.junit.jupiter.api.Assertions.assertNotNull;
2222
import static org.junit.jupiter.api.Assertions.assertNull;
23+
import static org.junit.jupiter.api.Assertions.assertSame;
2324
import static org.junit.jupiter.api.Assertions.assertThrows;
2425
import static org.junit.jupiter.api.Assertions.assertTrue;
2526
import static org.seasar.doma.it.criteria.CustomExpressions.addOne;
@@ -950,4 +951,24 @@ void iterableDomain() {
950951
assertEquals("TOKYO", iterator.next());
951952
assertEquals("KYOTO", iterator.next());
952953
}
954+
955+
@Test
956+
void selfJoin_association() {
957+
Employee_ e = new Employee_();
958+
Employee_ m = new Employee_();
959+
960+
QueryDsl queryDsl = new QueryDsl(config);
961+
List<Employee> employees =
962+
queryDsl
963+
.from(e)
964+
.leftJoin(m, on -> on.eq(e.managerId, m.employeeId))
965+
.where(c -> c.in(e.employeeId, List.of(6, 9)))
966+
.orderBy(c -> c.asc(e.employeeId))
967+
.associate(e, m, (Employee::setManager))
968+
.fetch();
969+
assertEquals(2, employees.size());
970+
Employee blake = employees.get(0);
971+
Employee king = employees.get(1);
972+
assertSame(king, blake.getManager());
973+
}
953974
}

0 commit comments

Comments
 (0)