Skip to content

Commit 1dad50f

Browse files
nakamura-toclaude
andauthored
feat: implement @Embedded annotation with column name prefix support (#1384)
* feat: implement @Embedded annotation with column name prefix support This comprehensive feature enables embedding value objects within entities with customizable column name prefixes, allowing the same embeddable type to be used multiple times in a single entity with different column naming. ## Core Features Implemented ### @Embedded Annotation - New @Embedded annotation with prefix attribute for column name prefixing - Comprehensive JavaDoc documentation with usage examples - Proper annotation meta-annotations (@target, @retention, @EntityField) ### Column Name Prefix Support - Extended DefaultPropertyType to support columnNamePrefix parameter - Updated EmbeddableType interface with new getEmbeddablePropertyTypes method - Backward compatibility maintained with deprecated method delegation ### Annotation Processor Integration - New EmbeddedAnnot class for annotation processing - Enhanced EntityPropertyMetaFactory to handle @Embedded annotations - Updated code generation to pass column prefixes to property types - Proper error handling for invalid @Embedded usage (DOMA4498) ### Comprehensive Test Coverage - Unit tests for DefaultPropertyType.columnNamePrefix functionality - Integration tests with Customer/CustomerAddress entities - Processor tests for annotation validation and code generation - Database compatibility tests across all supported databases ### Database Integration - Customer entity with embedded billing and shipping addresses - CustomerDao interface with insert/select operations - Complete SQL scripts for all supported databases (H2, MySQL, PostgreSQL, Oracle, SQLite, SQL Server, DB2, HSQLDB) - Integration test suite demonstrating real-world usage ## Technical Implementation ### Type System Integration - Enhanced EmbeddableTypeGenerator for prefix parameter passing - Updated EntityTypePropertyGenerator for embedded property creation - Seamless integration with existing entity/embeddable architecture ### Backward Compatibility - Existing @embeddable classes work without modification - Default empty prefix maintains current behavior - Deprecated methods provide smooth migration path ## Usage Example ```java @embeddable public record Address(String street, String city, String zipCode) {} @entity public class Customer { @id Integer customerId; @Embedded(prefix = "billing_") Address billingAddress; @Embedded(prefix = "shipping_") Address shippingAddress; } ``` This creates columns: billing_street, billing_city, billing_zip_code, shipping_street, shipping_city, shipping_zip_code. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: add CUSTOMER table to SQLite script for integration tests --------- Co-authored-by: Claude <[email protected]>
1 parent 3a2fd9a commit 1dad50f

File tree

51 files changed

+1125
-29
lines changed

Some content is hidden

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

51 files changed

+1125
-29
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 embedded property.
25+
*
26+
* <p>The annotated field must be a member of an {@link Entity} annotated class. The type of the
27+
* field must be a class annotated with {@link Embeddable}.
28+
*
29+
* <p>The {@link Embedded} annotation allows for embedding value objects within entities, enabling
30+
* composition of entities from smaller, reusable components.
31+
*
32+
* <pre>
33+
* &#064;Embeddable
34+
* public class Address {
35+
* String street;
36+
* String city;
37+
* String zipCode;
38+
* }
39+
*
40+
* &#064;Entity
41+
* public class Employee {
42+
*
43+
* &#064;Id
44+
* Integer id;
45+
*
46+
* String name;
47+
*
48+
* &#064;Embedded
49+
* Address address;
50+
*
51+
* &#064;Embedded(prefix = "home_")
52+
* Address homeAddress;
53+
* }
54+
* </pre>
55+
*/
56+
@Target(ElementType.FIELD)
57+
@Retention(RetentionPolicy.RUNTIME)
58+
@EntityField
59+
public @interface Embedded {
60+
61+
/**
62+
* The prefix for column names of the embedded properties.
63+
*
64+
* <p>When specified, the prefix is prepended to the column names of all properties within the
65+
* embedded object. This is useful when embedding the same embeddable type multiple times in an
66+
* entity.
67+
*
68+
* <p>For example, if an embeddable has a property mapped to column "street" and the prefix is
69+
* "home_", the resulting column name will be "home_street".
70+
*
71+
* @return the column name prefix
72+
*/
73+
String prefix() default "";
74+
}

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public class DefaultPropertyType<ENTITY, BASIC, CONTAINER>
6262

6363
protected final PropertyField<ENTITY> field;
6464

65+
protected final String columnNamePrefix;
66+
6567
public DefaultPropertyType(
6668
Class<ENTITY> entityClass,
6769
Supplier<Scalar<BASIC, CONTAINER>> scalarSupplier,
@@ -71,6 +73,28 @@ public DefaultPropertyType(
7173
boolean insertable,
7274
boolean updatable,
7375
boolean quoteRequired) {
76+
this(
77+
entityClass,
78+
scalarSupplier,
79+
name,
80+
columnName,
81+
namingType,
82+
insertable,
83+
updatable,
84+
quoteRequired,
85+
"");
86+
}
87+
88+
public DefaultPropertyType(
89+
Class<ENTITY> entityClass,
90+
Supplier<Scalar<BASIC, CONTAINER>> scalarSupplier,
91+
String name,
92+
String columnName,
93+
NamingType namingType,
94+
boolean insertable,
95+
boolean updatable,
96+
boolean quoteRequired,
97+
String columnNamePrefix) {
7498
if (entityClass == null) {
7599
throw new DomaNullPointerException("entityClass");
76100
}
@@ -83,6 +107,9 @@ public DefaultPropertyType(
83107
if (columnName == null) {
84108
throw new DomaNullPointerException("columnName");
85109
}
110+
if (columnNamePrefix == null) {
111+
throw new DomaNullPointerException("columnNamePrefix");
112+
}
86113
this.entityClass = entityClass;
87114
this.scalarSupplier = scalarSupplier;
88115
this.name = name;
@@ -94,6 +121,7 @@ public DefaultPropertyType(
94121
this.updatable = updatable;
95122
this.quoteRequired = quoteRequired;
96123
this.field = new PropertyField<>(name, entityClass);
124+
this.columnNamePrefix = columnNamePrefix;
97125
}
98126

99127
@Override
@@ -134,9 +162,11 @@ public String getColumnName(BiFunction<NamingType, String, String> namingFunctio
134162
public String getColumnName(
135163
BiFunction<NamingType, String, String> namingFunction,
136164
Function<String, String> quoteFunction) {
137-
String columnName = this.columnName;
138-
if (columnName.isEmpty()) {
139-
columnName = namingFunction.apply(namingType, simpleName);
165+
String columnName;
166+
if (this.columnName.isEmpty()) {
167+
columnName = columnNamePrefix + namingFunction.apply(namingType, simpleName);
168+
} else {
169+
columnName = columnNamePrefix + this.columnName;
140170
}
141171
return quoteRequired ? quoteFunction.apply(columnName) : columnName;
142172
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,34 @@ public interface EmbeddableType<EMBEDDABLE> {
4747
* @param namingType the naming convention used for column names
4848
* @return a list of entity property types for the embeddable properties
4949
*/
50+
@Deprecated
51+
default <ENTITY> List<EntityPropertyType<ENTITY, ?>> getEmbeddablePropertyTypes(
52+
String embeddedPropertyName, Class<ENTITY> entityClass, NamingType namingType) {
53+
return getEmbeddablePropertyTypes(embeddedPropertyName, entityClass, namingType, "");
54+
}
55+
56+
/**
57+
* Returns a list of entity property types for the embeddable properties with a column name
58+
* prefix.
59+
*
60+
* <p>This method is used to obtain metadata about the properties within an embeddable object when
61+
* it is embedded in an entity. The property types are used for mapping between the embeddable
62+
* object's properties and database columns. The column name prefix is applied to all column names
63+
* of the embeddable properties, which is useful when the same embeddable type is used multiple
64+
* times within an entity.
65+
*
66+
* @param <ENTITY> the entity type that contains the embeddable
67+
* @param embeddedPropertyName the name of the property in the entity that holds the embeddable
68+
* @param entityClass the entity class
69+
* @param namingType the naming convention used for column names
70+
* @param columNamePrefix the prefix to be prepended to column names of all embeddable properties
71+
* @return a list of entity property types for the embeddable properties
72+
*/
5073
<ENTITY> List<EntityPropertyType<ENTITY, ?>> getEmbeddablePropertyTypes(
51-
String embeddedPropertyName, Class<ENTITY> entityClass, NamingType namingType);
74+
String embeddedPropertyName,
75+
Class<ENTITY> entityClass,
76+
NamingType namingType,
77+
String columNamePrefix);
5278

5379
/**
5480
* Creates a new instance of the embeddable object.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,8 @@ public enum Message implements MessageResource {
10171017
DOMA4496(
10181018
"When \"returning = @Returning\" is specified, the return type must be a List of the entity class \"{0}\"."),
10191019
DOMA4497("The {0} type parameter of BiConsumer must be an entity class."),
1020+
DOMA4498(
1021+
"You cannot annotate the field with @Embedded if the field type is not an embeddable class."),
10201022

10211023
// other
10221024
DOMA5001(

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,135 @@ public void testWrapperPropertyDefaultValue() {
235235
assertNull(property.get());
236236
}
237237

238+
@Test
239+
public void testColumnNamePrefix_columnDefined() {
240+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
241+
new DefaultPropertyType<>(
242+
DefaultPropertyTypeTest.class,
243+
() -> new BasicScalar<>(StringWrapper::new),
244+
"hoge",
245+
"foo",
246+
NamingType.UPPER_CASE,
247+
true,
248+
true,
249+
false,
250+
"prefix_");
251+
assertEquals("prefix_foo", propertyType.getColumnName());
252+
}
253+
254+
@Test
255+
public void testColumnNamePrefix_columnNotDefined() {
256+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
257+
new DefaultPropertyType<>(
258+
DefaultPropertyTypeTest.class,
259+
() -> new BasicScalar<>(StringWrapper::new),
260+
"hoge",
261+
"",
262+
NamingType.UPPER_CASE,
263+
true,
264+
true,
265+
false,
266+
"prefix_");
267+
assertEquals("prefix_HOGE", propertyType.getColumnName());
268+
}
269+
270+
@Test
271+
public void testColumnNamePrefix_emptyString() {
272+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
273+
new DefaultPropertyType<>(
274+
DefaultPropertyTypeTest.class,
275+
() -> new BasicScalar<>(StringWrapper::new),
276+
"hoge",
277+
"foo",
278+
NamingType.UPPER_CASE,
279+
true,
280+
true,
281+
false,
282+
"");
283+
assertEquals("foo", propertyType.getColumnName());
284+
}
285+
286+
@Test
287+
public void testColumnNamePrefix_quoteRequired() {
288+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
289+
new DefaultPropertyType<>(
290+
DefaultPropertyTypeTest.class,
291+
() -> new BasicScalar<>(StringWrapper::new),
292+
"hoge",
293+
"foo",
294+
NamingType.UPPER_CASE,
295+
true,
296+
true,
297+
true,
298+
"prefix_");
299+
assertEquals("[prefix_foo]", propertyType.getColumnName(text -> "[" + text + "]"));
300+
}
301+
302+
@Test
303+
public void testColumnNamePrefix_quoteNotRequired() {
304+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
305+
new DefaultPropertyType<>(
306+
DefaultPropertyTypeTest.class,
307+
() -> new BasicScalar<>(StringWrapper::new),
308+
"hoge",
309+
"foo",
310+
NamingType.UPPER_CASE,
311+
true,
312+
true,
313+
false,
314+
"prefix_");
315+
assertEquals("prefix_foo", propertyType.getColumnName(text -> "[" + text + "]"));
316+
}
317+
318+
@Test
319+
public void testColumnNamePrefix_embeddableProperty() {
320+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
321+
new DefaultPropertyType<>(
322+
DefaultPropertyTypeTest.class,
323+
() -> new BasicScalar<>(StringWrapper::new),
324+
"foo.hoge",
325+
"",
326+
NamingType.UPPER_CASE,
327+
true,
328+
true,
329+
false,
330+
"prefix_");
331+
assertEquals("prefix_HOGE", propertyType.getColumnName());
332+
}
333+
334+
@Test
335+
public void testColumnNamePrefix_namingFunction() {
336+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
337+
new DefaultPropertyType<>(
338+
DefaultPropertyTypeTest.class,
339+
() -> new BasicScalar<>(StringWrapper::new),
340+
"hoge",
341+
"",
342+
NamingType.UPPER_CASE,
343+
true,
344+
true,
345+
false,
346+
"prefix_");
347+
assertEquals("prefix_HOGE", propertyType.getColumnName(NamingType::apply));
348+
}
349+
350+
@Test
351+
public void testColumnNamePrefix_namingAndQuoteFunction() {
352+
DefaultPropertyType<DefaultPropertyTypeTest, String, String> propertyType =
353+
new DefaultPropertyType<>(
354+
DefaultPropertyTypeTest.class,
355+
() -> new BasicScalar<>(StringWrapper::new),
356+
"hoge",
357+
"",
358+
NamingType.UPPER_CASE,
359+
true,
360+
true,
361+
true,
362+
"prefix_");
363+
assertEquals(
364+
"[prefix_HOGE]", propertyType.getColumnName(NamingType::apply, text -> "[" + text + "]"));
365+
}
366+
238367
public static class Foo {
239368
String hoge;
240369
}

doma-processor/src/main/java/org/seasar/doma/internal/apt/annot/Annotations.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.seasar.doma.Domain;
3939
import org.seasar.doma.DomainConverters;
4040
import org.seasar.doma.Embeddable;
41+
import org.seasar.doma.Embedded;
4142
import org.seasar.doma.Entity;
4243
import org.seasar.doma.ResultSet;
4344
import org.seasar.doma.SequenceGenerator;
@@ -274,6 +275,15 @@ public EmbeddableAnnot newEmbeddableAnnot(TypeElement typeElement) {
274275
return new EmbeddableAnnot(embeddableMirror, metamodelAnnot);
275276
}
276277

278+
/**
279+
* @param field non-null
280+
* @return nullable
281+
*/
282+
public EmbeddedAnnot newEmbeddedAnnot(VariableElement field) {
283+
assertNotNull(field);
284+
return newInstance(field, Embedded.class, EmbeddedAnnot::new);
285+
}
286+
277287
/**
278288
* @param typeElement non-null
279289
* @return nullable
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.apt.annot;
17+
18+
import static org.seasar.doma.internal.util.AssertionUtil.assertNonNullValue;
19+
20+
import java.util.Map;
21+
import javax.lang.model.element.AnnotationMirror;
22+
import javax.lang.model.element.AnnotationValue;
23+
import org.seasar.doma.internal.apt.AptIllegalStateException;
24+
import org.seasar.doma.internal.apt.util.AnnotationValueUtil;
25+
26+
public class EmbeddedAnnot extends AbstractAnnot {
27+
28+
public static final String PREFIX = "prefix";
29+
30+
private final AnnotationValue prefix;
31+
32+
EmbeddedAnnot(AnnotationMirror annotationMirror, Map<String, AnnotationValue> values) {
33+
super(annotationMirror);
34+
this.prefix = assertNonNullValue(values, PREFIX);
35+
}
36+
37+
public AnnotationValue getPrefix() {
38+
return prefix;
39+
}
40+
41+
public String getPrefixValue() {
42+
String value = AnnotationValueUtil.toString(prefix);
43+
if (value == null) {
44+
throw new AptIllegalStateException(PREFIX);
45+
}
46+
return value;
47+
}
48+
}

0 commit comments

Comments
 (0)