Skip to content

Commit 2f0c9a6

Browse files
authored
feat: add columnOverrides attribute to @Embedded annotation (#1386)
2 parents ca0802d + 70958d1 commit 2f0c9a6

30 files changed

+1089
-58
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
/**
19+
* Overrides the column mapping for a specific property within an embeddable object.
20+
*
21+
* <p>This annotation is used within the {@link Embedded} annotation to customize how properties of
22+
* an embeddable object are mapped to database columns. It allows you to override the default column
23+
* name and attributes (such as insertable, updatable, and quote) for individual properties when the
24+
* embeddable is embedded in an entity.
25+
*
26+
* <p>This is particularly useful when:
27+
*
28+
* <ul>
29+
* <li>The same embeddable type is used multiple times in an entity and requires different column
30+
* names to avoid conflicts
31+
* <li>The column names in the database don't match the default naming conventions
32+
* <li>You need to control the behavior of specific columns (e.g., making them read-only)
33+
* </ul>
34+
*
35+
* <pre>
36+
* &#064;Embeddable
37+
* public class Address {
38+
* String street;
39+
* String city;
40+
* String zipCode;
41+
* }
42+
*
43+
* &#064;Entity
44+
* public class Employee {
45+
* &#064;Id
46+
* Integer id;
47+
*
48+
* &#064;Embedded(columnOverrides = {
49+
* &#064;ColumnOverride(name = "street", column = &#064;Column(name = "HOME_STREET")),
50+
* &#064;ColumnOverride(name = "city", column = &#064;Column(name = "HOME_CITY")),
51+
* &#064;ColumnOverride(name = "zipCode", column = &#064;Column(name = "HOME_ZIP"))
52+
* })
53+
* Address homeAddress;
54+
*
55+
* &#064;Embedded(columnOverrides = {
56+
* &#064;ColumnOverride(name = "street", column = &#064;Column(name = "WORK_STREET")),
57+
* &#064;ColumnOverride(name = "city", column = &#064;Column(name = "WORK_CITY")),
58+
* &#064;ColumnOverride(name = "zipCode", column = &#064;Column(name = "WORK_ZIP"))
59+
* })
60+
* Address workAddress;
61+
* }
62+
* </pre>
63+
*
64+
* @see Embedded
65+
* @see Column
66+
* @see Embeddable
67+
*/
68+
public @interface ColumnOverride {
69+
/**
70+
* The name of the property in the embeddable class whose column mapping should be overridden.
71+
*
72+
* <p>This must match exactly the name of a field in the embeddable class.
73+
*
74+
* @return the property name in the embeddable class
75+
*/
76+
String name();
77+
78+
/**
79+
* The column definition that overrides the default column mapping for the specified property.
80+
*
81+
* <p>This allows you to specify a custom column name and control column attributes such as
82+
* insertable, updatable, and quote.
83+
*
84+
* @return the column definition to use for the property
85+
*/
86+
Column column();
87+
}

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
* <p>The {@link Embedded} annotation allows for embedding value objects within entities, enabling
3030
* composition of entities from smaller, reusable components.
3131
*
32+
* <h3>Basic Usage</h3>
33+
*
3234
* <pre>
3335
* &#064;Embeddable
3436
* public class Address {
@@ -39,19 +41,58 @@
3941
*
4042
* &#064;Entity
4143
* public class Employee {
42-
*
4344
* &#064;Id
4445
* Integer id;
4546
*
4647
* String name;
4748
*
4849
* &#064;Embedded
4950
* Address address;
51+
* }
52+
* </pre>
53+
*
54+
* <h3>Using Prefix</h3>
55+
*
56+
* <pre>
57+
* &#064;Entity
58+
* public class Person {
59+
* &#064;Id
60+
* Integer id;
5061
*
5162
* &#064;Embedded(prefix = "home_")
5263
* Address homeAddress;
64+
*
65+
* &#064;Embedded(prefix = "work_")
66+
* Address workAddress;
67+
* }
68+
* </pre>
69+
*
70+
* <h3>Using Column Overrides</h3>
71+
*
72+
* <pre>
73+
* &#064;Entity
74+
* public class Customer {
75+
* &#064;Id
76+
* Integer id;
77+
*
78+
* &#064;Embedded(columnOverrides = {
79+
* &#064;ColumnOverride(name = "street", column = &#064;Column(name = "BILLING_STREET")),
80+
* &#064;ColumnOverride(name = "city", column = &#064;Column(name = "BILLING_CITY")),
81+
* &#064;ColumnOverride(name = "zipCode", column = &#064;Column(name = "BILLING_ZIP"))
82+
* })
83+
* Address billingAddress;
84+
*
85+
* &#064;Embedded(columnOverrides = {
86+
* &#064;ColumnOverride(name = "street", column = &#064;Column(name = "SHIPPING_STREET")),
87+
* &#064;ColumnOverride(name = "city", column = &#064;Column(name = "SHIPPING_CITY")),
88+
* &#064;ColumnOverride(name = "zipCode", column = &#064;Column(name = "SHIPPING_ZIP", updatable = false))
89+
* })
90+
* Address shippingAddress;
5391
* }
5492
* </pre>
93+
*
94+
* @see Embeddable
95+
* @see ColumnOverride
5596
*/
5697
@Target(ElementType.FIELD)
5798
@Retention(RetentionPolicy.RUNTIME)
@@ -71,4 +112,30 @@
71112
* @return the column name prefix
72113
*/
73114
String prefix() default "";
115+
116+
/**
117+
* Column overrides for specific properties within the embedded object.
118+
*
119+
* <p>This allows fine-grained control over the column mappings of individual properties in the
120+
* embeddable. Each {@link ColumnOverride} specifies a property name and its corresponding column
121+
* definition, which overrides the default column mapping for that property.
122+
*
123+
* <p>Column overrides are particularly useful when:
124+
*
125+
* <ul>
126+
* <li>You need to map properties to columns with names that don't follow the standard naming
127+
* convention
128+
* <li>You want to control column attributes (insertable, updatable, quote) for specific
129+
* properties
130+
* <li>Different instances of the same embeddable require different column configurations
131+
* </ul>
132+
*
133+
* <p>Note that column overrides take precedence over the {@link #prefix()} attribute. If both are
134+
* specified, the column override will be used for the specified properties, while the prefix will
135+
* be applied to all other properties.
136+
*
137+
* @return an array of column overrides for properties in the embedded object
138+
* @see ColumnOverride
139+
*/
140+
ColumnOverride[] columnOverrides() default {};
74141
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
/**
19+
* Represents the metadata for a database column.
20+
*
21+
* <p>This record encapsulates the configuration of a column in a database table, including its name
22+
* and behavioral attributes. It is used internally by the Doma framework to manage column mappings
23+
* and SQL generation.
24+
*
25+
* <p>The column type information is typically derived from {@link org.seasar.doma.Column}
26+
* annotations or {@link org.seasar.doma.ColumnOverride} configurations, and is used to control how
27+
* entity properties are mapped to database columns.
28+
*
29+
* @param name the column name in the database
30+
* @param insertable whether the column should be included in SQL INSERT statements
31+
* @param updatable whether the column should be included in SQL UPDATE statements
32+
* @param quote whether the column name should be enclosed by quotation marks in SQL statements
33+
* @see org.seasar.doma.Column
34+
* @see org.seasar.doma.ColumnOverride
35+
*/
36+
public record ColumnType(String name, Boolean insertable, Boolean updatable, Boolean quote) {}

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

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ public class DefaultPropertyType<ENTITY, BASIC, CONTAINER>
6262

6363
protected final PropertyField<ENTITY> field;
6464

65-
protected final String columnNamePrefix;
65+
protected final String prefix;
66+
67+
protected final ColumnType columnType;
6668

6769
public DefaultPropertyType(
6870
Class<ENTITY> entityClass,
@@ -82,7 +84,7 @@ public DefaultPropertyType(
8284
insertable,
8385
updatable,
8486
quoteRequired,
85-
"");
87+
null);
8688
}
8789

8890
public DefaultPropertyType(
@@ -94,7 +96,7 @@ public DefaultPropertyType(
9496
boolean insertable,
9597
boolean updatable,
9698
boolean quoteRequired,
97-
String columnNamePrefix) {
99+
EmbeddedType embeddedType) {
98100
if (entityClass == null) {
99101
throw new DomaNullPointerException("entityClass");
100102
}
@@ -107,21 +109,31 @@ public DefaultPropertyType(
107109
if (columnName == null) {
108110
throw new DomaNullPointerException("columnName");
109111
}
110-
if (columnNamePrefix == null) {
111-
throw new DomaNullPointerException("columnNamePrefix");
112-
}
113112
this.entityClass = entityClass;
114113
this.scalarSupplier = scalarSupplier;
115114
this.name = name;
116115
int pos = name.lastIndexOf('.');
117116
this.simpleName = pos > -1 ? name.substring(pos + 1) : name;
118-
this.columnName = columnName;
119117
this.namingType = namingType;
120-
this.insertable = insertable;
121-
this.updatable = updatable;
122-
this.quoteRequired = quoteRequired;
118+
if (embeddedType == null) {
119+
this.prefix = "";
120+
columnType = null;
121+
} else {
122+
this.prefix = embeddedType.prefix();
123+
columnType = embeddedType.columnTypeMap().get(simpleName);
124+
}
125+
if (columnType == null) {
126+
this.columnName = columnName;
127+
this.insertable = insertable;
128+
this.updatable = updatable;
129+
this.quoteRequired = quoteRequired;
130+
} else {
131+
this.columnName = columnType.name() != null ? columnType.name() : columnName;
132+
this.insertable = columnType.insertable() != null ? columnType.insertable() : insertable;
133+
this.updatable = columnType.updatable() != null ? columnType.updatable() : updatable;
134+
this.quoteRequired = columnType.quote() != null ? columnType.quote() : quoteRequired;
135+
}
123136
this.field = new PropertyField<>(name, entityClass);
124-
this.columnNamePrefix = columnNamePrefix;
125137
}
126138

127139
@Override
@@ -164,9 +176,14 @@ public String getColumnName(
164176
Function<String, String> quoteFunction) {
165177
String columnName;
166178
if (this.columnName.isEmpty()) {
167-
columnName = columnNamePrefix + namingFunction.apply(namingType, simpleName);
179+
columnName = prefix + namingFunction.apply(namingType, simpleName);
168180
} else {
169-
columnName = columnNamePrefix + this.columnName;
181+
if (columnType != null) {
182+
// column overrides take precedence over the prefix attribute
183+
columnName = this.columnName;
184+
} else {
185+
columnName = prefix + this.columnName;
186+
}
170187
}
171188
return quoteRequired ? quoteFunction.apply(columnName) : columnName;
172189
}

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

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

18+
import java.util.Collections;
1819
import java.util.List;
1920
import java.util.Map;
2021

@@ -50,7 +51,11 @@ public interface EmbeddableType<EMBEDDABLE> {
5051
@Deprecated
5152
default <ENTITY> List<EntityPropertyType<ENTITY, ?>> getEmbeddablePropertyTypes(
5253
String embeddedPropertyName, Class<ENTITY> entityClass, NamingType namingType) {
53-
return getEmbeddablePropertyTypes(embeddedPropertyName, entityClass, namingType, "");
54+
return getEmbeddablePropertyTypes(
55+
embeddedPropertyName,
56+
entityClass,
57+
namingType,
58+
new EmbeddedType("", Collections.emptyMap()));
5459
}
5560

5661
/**
@@ -67,14 +72,14 @@ public interface EmbeddableType<EMBEDDABLE> {
6772
* @param embeddedPropertyName the name of the property in the entity that holds the embeddable
6873
* @param entityClass the entity class
6974
* @param namingType the naming convention used for column names
70-
* @param columNamePrefix the prefix to be prepended to column names of all embeddable properties
75+
* @param embeddedType the embedded type metadata containing column prefix and column overrides
7176
* @return a list of entity property types for the embeddable properties
7277
*/
7378
<ENTITY> List<EntityPropertyType<ENTITY, ?>> getEmbeddablePropertyTypes(
7479
String embeddedPropertyName,
7580
Class<ENTITY> entityClass,
7681
NamingType namingType,
77-
String columNamePrefix);
82+
EmbeddedType embeddedType);
7883

7984
/**
8085
* Creates a new instance of the embeddable object.

0 commit comments

Comments
 (0)