Skip to content

Commit dd732b2

Browse files
authored
feat: Add support for nested embeddable classes (#1400)
2 parents 3f0ea74 + bd838cc commit dd732b2

File tree

47 files changed

+1550
-341
lines changed

Some content is hidden

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

47 files changed

+1550
-341
lines changed

doma-core/src/main/java/org/seasar/doma/jdbc/entity/DefaultPropertyType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public DefaultPropertyType(
120120
columnType = null;
121121
} else {
122122
this.prefix = embeddedType.prefix();
123-
columnType = embeddedType.columnTypeMap().get(simpleName);
123+
columnType = embeddedType.columnTypeMap().get(name);
124124
}
125125
if (columnType == null) {
126126
this.columnName = columnName;

doma-core/src/main/java/org/seasar/doma/jdbc/entity/EmbeddedType.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.seasar.doma.jdbc.entity;
1717

18+
import java.util.Collections;
19+
import java.util.HashMap;
1820
import java.util.Map;
1921
import java.util.Objects;
2022

@@ -55,4 +57,31 @@ public record EmbeddedType(String prefix, Map<String, ColumnType> columnTypeMap)
5557
Objects.requireNonNull(prefix);
5658
Objects.requireNonNull(columnTypeMap);
5759
}
60+
61+
/**
62+
* Merges this EmbeddedType with a parent EmbeddedType to create a new EmbeddedType instance.
63+
*
64+
* <p>This method is used to handle nested embeddable objects. When an embeddable object contains
65+
* another embeddable object, this method combines the configuration from both the parent and
66+
* child embedded types.
67+
*
68+
* <p>The merging process combines:
69+
*
70+
* <ul>
71+
* <li>Prefixes are concatenated (parent prefix + this prefix)
72+
* <li>Column type maps are merged with parent mappings taking precedence over child mappings
73+
* </ul>
74+
*
75+
* @param parent the parent EmbeddedType to merge with, can be null
76+
* @return a new EmbeddedType with merged configuration, or this instance if parent is null
77+
*/
78+
public EmbeddedType merge(EmbeddedType parent) {
79+
if (parent == null) {
80+
return this;
81+
}
82+
var prefix = parent.prefix + this.prefix;
83+
var columnTypeMap = new HashMap<>(this.columnTypeMap);
84+
columnTypeMap.putAll(parent.columnTypeMap);
85+
return new EmbeddedType(prefix, Collections.unmodifiableMap(columnTypeMap));
86+
}
5887
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,7 @@ public enum Message implements MessageResource {
10201020
DOMA4498(
10211021
"You cannot annotate the field with @Embedded if the field type is not an embeddable class."),
10221022
DOMA4499("The property \"{0}\" is not found in the embeddable class \"{1}\"."),
1023+
DOMA4500("The property \"{0}\" refers to the class \"{1}\", which creates a circular reference."),
10231024

10241025
// other
10251026
DOMA5001(

doma-core/src/test/java/org/seasar/doma/jdbc/entity/DefaultPropertyTypeTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ public void testColumnNamePrefix_namingAndQuoteFunction() {
371371
@Test
372372
public void testColumnTypeMap_overridesColumnName() {
373373
ColumnType columnType = new ColumnType("CUSTOM_COLUMN", null, null, null);
374-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
374+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
375375
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
376376
new DefaultPropertyType<>(
377377
DefaultPropertyTypeTest.class,
@@ -389,7 +389,7 @@ public void testColumnTypeMap_overridesColumnName() {
389389
@Test
390390
public void testColumnTypeMap_overridesInsertable() {
391391
ColumnType columnType = new ColumnType("CUSTOM_COLUMN", false, null, null);
392-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
392+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
393393
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
394394
new DefaultPropertyType<>(
395395
DefaultPropertyTypeTest.class,
@@ -407,7 +407,7 @@ public void testColumnTypeMap_overridesInsertable() {
407407
@Test
408408
public void testColumnTypeMap_overridesUpdatable() {
409409
ColumnType columnType = new ColumnType("CUSTOM_COLUMN", null, false, null);
410-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
410+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
411411
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
412412
new DefaultPropertyType<>(
413413
DefaultPropertyTypeTest.class,
@@ -425,7 +425,7 @@ public void testColumnTypeMap_overridesUpdatable() {
425425
@Test
426426
public void testColumnTypeMap_overridesQuote() {
427427
ColumnType columnType = new ColumnType("CUSTOM_COLUMN", null, null, true);
428-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
428+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
429429
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
430430
new DefaultPropertyType<>(
431431
DefaultPropertyTypeTest.class,
@@ -443,7 +443,7 @@ public void testColumnTypeMap_overridesQuote() {
443443
@Test
444444
public void testColumnTypeMap_ignoresPrefixWhenOverridden() {
445445
ColumnType columnType = new ColumnType("CUSTOM_COLUMN", null, null, null);
446-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
446+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
447447
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
448448
new DefaultPropertyType<>(
449449
DefaultPropertyTypeTest.class,
@@ -481,7 +481,7 @@ public void testColumnTypeMap_noOverrideForProperty() {
481481
@Test
482482
public void testColumnTypeMap_multipleOverrides() {
483483
ColumnType columnType = new ColumnType("OVERRIDDEN_NAME", false, true, true);
484-
Map<String, ColumnType> columnTypeMap = Map.of("hoge", columnType);
484+
Map<String, ColumnType> columnTypeMap = Map.of("foo.hoge", columnType);
485485
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
486486
new DefaultPropertyType<>(
487487
DefaultPropertyTypeTest.class,
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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.entity;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotSame;
20+
import static org.junit.jupiter.api.Assertions.assertSame;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import java.util.Collections;
25+
import java.util.Map;
26+
import org.junit.jupiter.api.Test;
27+
28+
class EmbeddedTypeTest {
29+
30+
@Test
31+
void testConstructor_validParameters() {
32+
String prefix = "test_";
33+
Map<String, ColumnType> columnTypeMap =
34+
Map.of("prop1", new ColumnType("col1", null, null, null));
35+
36+
EmbeddedType embeddedType = new EmbeddedType(prefix, columnTypeMap);
37+
38+
assertEquals(prefix, embeddedType.prefix());
39+
assertEquals(columnTypeMap, embeddedType.columnTypeMap());
40+
}
41+
42+
@Test
43+
void testConstructor_emptyPrefix() {
44+
String prefix = "";
45+
Map<String, ColumnType> columnTypeMap = Collections.emptyMap();
46+
47+
EmbeddedType embeddedType = new EmbeddedType(prefix, columnTypeMap);
48+
49+
assertEquals(prefix, embeddedType.prefix());
50+
assertEquals(columnTypeMap, embeddedType.columnTypeMap());
51+
}
52+
53+
@Test
54+
void testConstructor_nullPrefix_throwsException() {
55+
Map<String, ColumnType> columnTypeMap = Collections.emptyMap();
56+
57+
assertThrows(NullPointerException.class, () -> new EmbeddedType(null, columnTypeMap));
58+
}
59+
60+
@Test
61+
void testConstructor_nullColumnTypeMap_throwsException() {
62+
String prefix = "test_";
63+
64+
assertThrows(NullPointerException.class, () -> new EmbeddedType(prefix, null));
65+
}
66+
67+
@Test
68+
void testMerge_withNullParent_returnsSameInstance() {
69+
String prefix = "child_";
70+
Map<String, ColumnType> columnTypeMap =
71+
Map.of("prop1", new ColumnType("col1", null, null, null));
72+
EmbeddedType child = new EmbeddedType(prefix, columnTypeMap);
73+
74+
EmbeddedType result = child.merge(null);
75+
76+
assertSame(child, result);
77+
}
78+
79+
@Test
80+
void testMerge_concatenatesPrefixes() {
81+
String parentPrefix = "parent_";
82+
String childPrefix = "child_";
83+
EmbeddedType parent = new EmbeddedType(parentPrefix, Collections.emptyMap());
84+
EmbeddedType child = new EmbeddedType(childPrefix, Collections.emptyMap());
85+
86+
EmbeddedType result = child.merge(parent);
87+
88+
assertEquals("parent_child_", result.prefix());
89+
}
90+
91+
@Test
92+
void testMerge_emptyPrefixes() {
93+
String parentPrefix = "";
94+
String childPrefix = "";
95+
EmbeddedType parent = new EmbeddedType(parentPrefix, Collections.emptyMap());
96+
EmbeddedType child = new EmbeddedType(childPrefix, Collections.emptyMap());
97+
98+
EmbeddedType result = child.merge(parent);
99+
100+
assertEquals("", result.prefix());
101+
}
102+
103+
@Test
104+
void testMerge_parentEmptyPrefix() {
105+
String parentPrefix = "";
106+
String childPrefix = "child_";
107+
EmbeddedType parent = new EmbeddedType(parentPrefix, Collections.emptyMap());
108+
EmbeddedType child = new EmbeddedType(childPrefix, Collections.emptyMap());
109+
110+
EmbeddedType result = child.merge(parent);
111+
112+
assertEquals("child_", result.prefix());
113+
}
114+
115+
@Test
116+
void testMerge_childEmptyPrefix() {
117+
String parentPrefix = "parent_";
118+
String childPrefix = "";
119+
EmbeddedType parent = new EmbeddedType(parentPrefix, Collections.emptyMap());
120+
EmbeddedType child = new EmbeddedType(childPrefix, Collections.emptyMap());
121+
122+
EmbeddedType result = child.merge(parent);
123+
124+
assertEquals("parent_", result.prefix());
125+
}
126+
127+
@Test
128+
void testMerge_mergesColumnTypeMaps_parentOverridesChild() {
129+
ColumnType childColumnType = new ColumnType("child_col", true, false, false);
130+
ColumnType parentColumnType = new ColumnType("parent_col", false, true, true);
131+
ColumnType childOnlyColumnType = new ColumnType("child_only_col", null, null, null);
132+
ColumnType parentOnlyColumnType = new ColumnType("parent_only_col", null, null, null);
133+
134+
Map<String, ColumnType> childMap =
135+
Map.of(
136+
"common_prop", childColumnType,
137+
"child_only_prop", childOnlyColumnType);
138+
Map<String, ColumnType> parentMap =
139+
Map.of(
140+
"common_prop", parentColumnType,
141+
"parent_only_prop", parentOnlyColumnType);
142+
143+
EmbeddedType parent = new EmbeddedType("parent_", parentMap);
144+
EmbeddedType child = new EmbeddedType("child_", childMap);
145+
146+
EmbeddedType result = child.merge(parent);
147+
148+
Map<String, ColumnType> resultMap = result.columnTypeMap();
149+
assertEquals(3, resultMap.size());
150+
assertEquals(parentColumnType, resultMap.get("common_prop")); // Parent overrides child
151+
assertEquals(childOnlyColumnType, resultMap.get("child_only_prop"));
152+
assertEquals(parentOnlyColumnType, resultMap.get("parent_only_prop"));
153+
}
154+
155+
@Test
156+
void testMerge_emptyColumnTypeMaps() {
157+
EmbeddedType parent = new EmbeddedType("parent_", Collections.emptyMap());
158+
EmbeddedType child = new EmbeddedType("child_", Collections.emptyMap());
159+
160+
EmbeddedType result = child.merge(parent);
161+
162+
assertTrue(result.columnTypeMap().isEmpty());
163+
}
164+
165+
@Test
166+
void testMerge_parentEmptyColumnTypeMap() {
167+
ColumnType childColumnType = new ColumnType("child_col", null, null, null);
168+
Map<String, ColumnType> childMap = Map.of("prop1", childColumnType);
169+
170+
EmbeddedType parent = new EmbeddedType("parent_", Collections.emptyMap());
171+
EmbeddedType child = new EmbeddedType("child_", childMap);
172+
173+
EmbeddedType result = child.merge(parent);
174+
175+
assertEquals(1, result.columnTypeMap().size());
176+
assertEquals(childColumnType, result.columnTypeMap().get("prop1"));
177+
}
178+
179+
@Test
180+
void testMerge_childEmptyColumnTypeMap() {
181+
ColumnType parentColumnType = new ColumnType("parent_col", null, null, null);
182+
Map<String, ColumnType> parentMap = Map.of("prop1", parentColumnType);
183+
184+
EmbeddedType parent = new EmbeddedType("parent_", parentMap);
185+
EmbeddedType child = new EmbeddedType("child_", Collections.emptyMap());
186+
187+
EmbeddedType result = child.merge(parent);
188+
189+
assertEquals(1, result.columnTypeMap().size());
190+
assertEquals(parentColumnType, result.columnTypeMap().get("prop1"));
191+
}
192+
193+
@Test
194+
void testMerge_returnsImmutableColumnTypeMap() {
195+
ColumnType columnType = new ColumnType("col", null, null, null);
196+
Map<String, ColumnType> parentMap = Map.of("prop1", columnType);
197+
198+
EmbeddedType parent = new EmbeddedType("parent_", parentMap);
199+
EmbeddedType child = new EmbeddedType("child_", Collections.emptyMap());
200+
201+
EmbeddedType result = child.merge(parent);
202+
203+
assertThrows(
204+
UnsupportedOperationException.class,
205+
() -> result.columnTypeMap().put("new_prop", new ColumnType("new_col", null, null, null)));
206+
}
207+
208+
@Test
209+
void testMerge_returnsNewInstance() {
210+
EmbeddedType parent = new EmbeddedType("parent_", Collections.emptyMap());
211+
EmbeddedType child = new EmbeddedType("child_", Collections.emptyMap());
212+
213+
EmbeddedType result = child.merge(parent);
214+
215+
assertNotSame(child, result);
216+
assertNotSame(parent, result);
217+
}
218+
219+
@Test
220+
void testMerge_complexScenario() {
221+
// Child has multiple properties with various overrides
222+
ColumnType childCol1 = new ColumnType("child_col1", true, false, false);
223+
ColumnType childCol2 = new ColumnType("child_col2", false, true, true);
224+
Map<String, ColumnType> childMap =
225+
Map.of(
226+
"prop1", childCol1,
227+
"prop2", childCol2);
228+
229+
// Parent has overlapping and additional properties
230+
ColumnType parentCol1 = new ColumnType("parent_col1", false, true, true); // Overrides child
231+
ColumnType parentCol3 = new ColumnType("parent_col3", null, null, false);
232+
Map<String, ColumnType> parentMap =
233+
Map.of(
234+
"prop1", parentCol1, // This should override child's prop1
235+
"prop3", parentCol3);
236+
237+
EmbeddedType parent = new EmbeddedType("parent_", parentMap);
238+
EmbeddedType child = new EmbeddedType("child_", childMap);
239+
240+
EmbeddedType result = child.merge(parent);
241+
242+
assertEquals("parent_child_", result.prefix());
243+
assertEquals(3, result.columnTypeMap().size());
244+
assertEquals(parentCol1, result.columnTypeMap().get("prop1")); // Parent override
245+
assertEquals(childCol2, result.columnTypeMap().get("prop2")); // Child only
246+
assertEquals(parentCol3, result.columnTypeMap().get("prop3")); // Parent only
247+
}
248+
}

0 commit comments

Comments
 (0)