Skip to content

Commit 9ab1ebb

Browse files
committed
HHH-4396 - Ability to patternize embedded column names
1 parent 06f16f9 commit 9ab1ebb

File tree

17 files changed

+946
-191
lines changed

17 files changed

+946
-191
lines changed

documentation/src/main/asciidoc/userguide/chapters/domain/embeddables.adoc

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,147 @@ include::{extrasdir}/embeddable/embeddable-type-override-mapping-example.sql[]
134134
----
135135
====
136136

137+
[[embeddable-column-naming]]
138+
==== @EmbeddedColumnNaming
139+
140+
The most common use case for `@AttributeOverride` in relation to an embeddable is to rename the associated columns.
141+
142+
Consider a typical embeddable mapping -
143+
144+
[[embeddable-column-naming-example-model]]
145+
.Typical embeddable mapping
146+
====
147+
[source,java]
148+
----
149+
@Entity
150+
class Person {
151+
// ...
152+
@Embedded
153+
Address homeAddress;
154+
@Embedded
155+
Address workAddress;
156+
}
157+
158+
@Embeddable
159+
class Address {
160+
String street;
161+
String city;
162+
// ...
163+
}
164+
----
165+
====
166+
167+
In strict Jakarta Persistence sense, this will lead to a bootstrapping error because
168+
Jakarta Persistence requires that the implicit names for the columns for both of the
169+
embedded `Address` mappings to be based on the attribute names from the embeddable -
170+
`street`, `city`, etc. However, that will lead to duplicate column names here.
171+
172+
The strict compliance way to accomplish this would be a tedious use of the `@AttributeOverride` annotation
173+
as <<embeddable-override,discussed previously>>.
174+
175+
Since this is such a common pattern, Hibernate offers a much simpler solution through its `@EmbeddedColumnNaming` annotation
176+
which allows to "patternize" the column naming -
177+
178+
[[embeddable-column-naming-example-basic]]
179+
.Simple `@EmbeddedColumnNaming` example
180+
====
181+
[source,java]
182+
----
183+
@Entity
184+
class Person {
185+
// ...
186+
@Embedded
187+
@EmbeddedColumnNaming("home_%s")
188+
Address homeAddress;
189+
@Embedded
190+
@EmbeddedColumnNaming("work_%s")
191+
Address workAddress;
192+
}
193+
----
194+
====
195+
196+
This mapping produces implicit column names `home_street`, `home_city`, `work_street`, `work_city`, etc.
197+
198+
`@EmbeddedColumnNaming` also works in nested usages and plays nicely with explicit column names.
199+
200+
[[embeddable-column-naming-example-column]]
201+
.Explicit @Column example
202+
====
203+
[source,java]
204+
----
205+
@Entity
206+
class Person {
207+
// ...
208+
@Embedded
209+
@EmbeddedColumnNaming("home_%s")
210+
Address homeAddress;
211+
@Embedded
212+
@EmbeddedColumnNaming("work_%s")
213+
Address workAddress;
214+
}
215+
216+
@Embeddable
217+
class Address {
218+
String street;
219+
String city;
220+
@Embedded
221+
private ZipPlus zip;
222+
// ...
223+
}
224+
225+
@Embeddable
226+
public static class ZipPlus {
227+
@Column(name="zip_code")
228+
private String code;
229+
@Column(name="zip_plus4")
230+
private String plus4;
231+
}
232+
----
233+
====
234+
235+
This will produce implicit column names `home_street`, `home_city`, `home_zip_code`, `home_zip_plus4`, ...
236+
237+
238+
When `@EmbeddedColumnNaming` is used withing nested embeddables, the affect will be cumulative.
239+
Given the following model:
240+
241+
[[embeddable-column-naming-example-cumulative]]
242+
.Cumulative @EmbeddedColumnNaming example
243+
====
244+
[source,java]
245+
----
246+
@Entity
247+
class Person {
248+
// ...
249+
@Embedded
250+
@EmbeddedColumnNaming("home_%s")
251+
Address homeAddress;
252+
@Embedded
253+
@EmbeddedColumnNaming("work_%s")
254+
Address workAddress;
255+
}
256+
257+
@Embeddable
258+
class Address {
259+
String street;
260+
String city;
261+
@Embedded
262+
@EmbeddedColumnNaming("zip_%s")
263+
private ZipPlus zip;
264+
// ...
265+
}
266+
267+
@Embeddable
268+
public static class ZipPlus {
269+
private String code;
270+
private String plus4;
271+
}
272+
----
273+
====
274+
275+
Here we will end up with the columns `home_street`, `home_city`, `home_zip_code`, `home_zip_plus4`, ...
276+
277+
137278

138279
[[embeddable-collections]]
139280
==== Collections of embeddable types

documentation/src/main/asciidoc/userguide/chapters/domain/naming.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ to specify the ImplicitNamingStrategy to use. See
6565
<<chapters/bootstrap/Bootstrap.adoc#bootstrap,Bootstrap>> for additional details on bootstrapping.
6666

6767

68+
[NOTE]
69+
.@EmbeddedColumnNaming
70+
====
71+
A related topic is the use of `@EmbeddedColumnNaming` to help with the implicit naming of columns associated with mapping an embeddable.
72+
See <<bnb,the disucussion>>
73+
====
6874

6975
[[PhysicalNamingStrategy]]
7076
==== PhysicalNamingStrategy
Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/*
2-
* Hibernate, Relational Persistence for Idiomatic Java
3-
*
4-
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5-
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
64
*/
75
package org.hibernate.annotations;
86

7+
import org.hibernate.Incubating;
8+
99
import java.lang.annotation.Retention;
1010
import java.lang.annotation.Target;
1111

@@ -16,17 +16,67 @@
1616
/**
1717
* Allows specifying a pattern to be applied to the naming of columns for
1818
* a particular {@linkplain jakarta.persistence.Embedded embedded mapping}.
19+
* For example, given a typical embeddable named {@code Address} and
20+
* {@code @EmbeddedColumnNaming("home_%s)}, we will get columns named
21+
* {@code home_street}, {@code home_city}, etc.
22+
* <p/>
23+
* Explicit {@linkplain jakarta.persistence.Column @Column(name)} mappings are incorporated
24+
* into the result. When embeddables are nested, the affect will be cumulative. Given the following model:
25+
*
26+
* <pre>
27+
* &#64;Entity
28+
* class Person {
29+
* ...
30+
* &#64;Embedded
31+
* &#64;EmbeddedColumnNaming("home_%s")
32+
* Address homeAddress;
33+
* &#64;Embedded
34+
* &#64;EmbeddedColumnNaming("work_%s")
35+
* Address workAddress;
36+
* }
37+
*
38+
* &#64;Embeddable
39+
* class Address {
40+
* &#64;Column(name="line1")
41+
* String street;
42+
* ...
43+
* &#64;Embedded
44+
* &#64;EmbeddedColumnNaming("zip_%s")
45+
* ZipPlus4 zip;
46+
* }
47+
*
48+
* &#64;Embeddable
49+
* class ZipPlus4 {
50+
* &#64;Column(name="zip_code")
51+
* String zipCode;
52+
* &#64;Column(name="plus_code")
53+
* String plusCode;
54+
* }
55+
* </pre>
56+
* Will result in the following columns:<ol>
57+
* <li>{@code home_line1}</li>
58+
* <li>{@code home_zip_zip_code}</li>
59+
* <li>{@code home_zip_plus_code}</li>
60+
* <li>{@code work_line1}</li>
61+
* <li>{@code work_zip_zip_code}</li>
62+
* <li>{@code work_zip_plus_code}</li>
63+
* </ol>
1964
*
65+
* @since 7.0
2066
* @author Steve Ebersole
2167
*/
2268
@Target({METHOD, FIELD})
2369
@Retention(RUNTIME)
70+
@Incubating
2471
public @interface EmbeddedColumnNaming {
2572
/**
2673
* The naming pattern. It is expected to contain a single pattern marker ({@code %})
27-
* into which the "raw" column name will be injected. E.g., given a typical {@code Address}
28-
* embeddable and {@code @Embedded @EmbeddedColumnNaming("home_%s)}, we will get columns named
29-
* {@code home_street}, {@code home_city}, etc.
74+
* into which the "raw" column name will be injected.
75+
* <p/>
76+
* The {@code value} may be omitted which will indicate to use the pattern
77+
* {@code "{ATTRIBUTE_NAME}_%s"} where {@code {ATTRIBUTE_NAME}} is the name of the attribute
78+
* where the annotation is placed - e.g. {@code @Embedded @EmbeddedColumnNaming Address homeAddress}
79+
* would create columns {@code homeAddress_street}, {@code homeAddress_city}, etc.
3080
*/
31-
String value();
81+
String value() default "";
3282
}

hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,18 +342,61 @@ public boolean isNameDeferred() {
342342
}
343343

344344
public void redefineColumnName(String columnName, String propertyName, boolean applyNamingStrategy) {
345-
if ( isNotEmpty( columnName ) ) {
346-
mappingColumn.setName( processColumnName( columnName, applyNamingStrategy ) );
345+
if ( StringHelper.isEmpty( columnName ) && StringHelper.isEmpty( propertyName ) ) {
346+
// nothing to do
347+
return;
348+
}
349+
final String logicalColumnName = resolveLogicalColumnName( columnName, propertyName );
350+
mappingColumn.setName( processColumnName( logicalColumnName, applyNamingStrategy ) );
351+
}
352+
353+
private String resolveLogicalColumnName(String columnName, String propertyName) {
354+
final String baseColumnName = StringHelper.isNotEmpty( columnName )
355+
? columnName
356+
: inferColumnName( propertyName );
357+
358+
if ( parent.getPropertyHolder() != null && parent.getPropertyHolder().isComponent() ) {
359+
// see if we need to apply one-or-more @EmbeddedColumnNaming patterns
360+
return applyEmbeddedColumnNaming( baseColumnName, (ComponentPropertyHolder) parent.getPropertyHolder() );
347361
}
348362
else {
349-
if ( propertyName != null && applyNamingStrategy ) {
350-
mappingColumn.setName( inferColumnName( propertyName ) );
363+
return baseColumnName;
364+
}
365+
}
366+
367+
private String applyEmbeddedColumnNaming(String inferredColumnName, ComponentPropertyHolder propertyHolder) {
368+
// code
369+
String result = inferredColumnName;
370+
boolean appliedAnyPatterns = false;
371+
372+
final String columnNamingPattern = propertyHolder.getComponent().getColumnNamingPattern();
373+
if ( StringHelper.isNotEmpty( columnNamingPattern ) ) {
374+
// zip_code
375+
result = String.format( columnNamingPattern, result );
376+
appliedAnyPatterns = true;
377+
}
378+
379+
ComponentPropertyHolder tester = propertyHolder;
380+
while ( tester.parent.isComponent() ) {
381+
final ComponentPropertyHolder parentHolder = (ComponentPropertyHolder) tester.parent;
382+
final String parentColumnNamingPattern = parentHolder.getComponent().getColumnNamingPattern();
383+
if ( StringHelper.isNotEmpty( parentColumnNamingPattern ) ) {
384+
// home_zip_code
385+
result = String.format( parentColumnNamingPattern, result );
386+
appliedAnyPatterns = true;
351387
}
352-
//Do nothing otherwise
388+
tester = parentHolder;
353389
}
390+
391+
if ( appliedAnyPatterns ) {
392+
// we need to adjust the logical name to be picked up in `#addColumnBinding`
393+
this.logicalColumnName = result;
394+
}
395+
396+
return result;
354397
}
355398

356-
private String processColumnName(String columnName, boolean applyNamingStrategy) {
399+
protected String processColumnName(String columnName, boolean applyNamingStrategy) {
357400
if ( applyNamingStrategy ) {
358401
final Database database = getBuildingContext().getMetadataCollector().getDatabase();
359402
return getBuildingContext().getBuildingOptions().getPhysicalNamingStrategy()
@@ -366,7 +409,7 @@ private String processColumnName(String columnName, boolean applyNamingStrategy)
366409

367410
}
368411

369-
private String inferColumnName(String propertyName) {
412+
protected String inferColumnName(String propertyName) {
370413
final Database database = getBuildingContext().getMetadataCollector().getDatabase();
371414
final ObjectNameNormalizer normalizer = getBuildingContext().getObjectNameNormalizer();
372415
final ImplicitNamingStrategy implicitNamingStrategy = getBuildingContext().getBuildingOptions().getImplicitNamingStrategy();

hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.hibernate.boot.spi.InFlightMetadataCollector;
1414
import org.hibernate.boot.spi.MetadataBuildingContext;
1515
import org.hibernate.boot.spi.PropertyData;
16+
import org.hibernate.internal.util.StringHelper;
1617
import org.hibernate.mapping.Column;
1718
import org.hibernate.mapping.PersistentClass;
1819
import org.hibernate.mapping.SimpleValue;
@@ -452,7 +453,9 @@ public void overrideFromReferencedColumnIfNecessary(Column column) {
452453

453454
@Override
454455
public void redefineColumnName(String columnName, String propertyName, boolean applyNamingStrategy) {
455-
super.redefineColumnName( columnName, null, applyNamingStrategy );
456+
if ( StringHelper.isNotEmpty( columnName ) ) {
457+
getMappingColumn().setName( processColumnName( columnName, applyNamingStrategy ) );
458+
}
456459
}
457460

458461
static AnnotatedJoinColumn buildImplicitJoinTableJoinColumn(

hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ public ComponentPropertyHolder(
9696
}
9797
}
9898

99+
/**
100+
* Access to the underlying component
101+
*/
102+
public Component getComponent() {
103+
return component;
104+
}
105+
99106
/**
100107
* This is called from our constructor and handles (in order):<ol>
101108
* <li>@Convert annotation at the Embeddable class level</li>

0 commit comments

Comments
 (0)