Skip to content

Commit

Permalink
HHH-14885 - New composite user-type
Browse files Browse the repository at this point in the history
Working support for `@EmbeddableInstantiator` on either the embedded site or on the embeddable class.
  • Loading branch information
sebersole committed Dec 1, 2021
1 parent 8ab27a0 commit 924c2b2
Show file tree
Hide file tree
Showing 28 changed files with 674 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[[embeddables]]
=== Embeddable types
:rootProjectDir: ../../../../../../..
:sourcedir: ../../../../../test/java/org/hibernate/userguide/mapping/embeddable
:coreProjectDir: {rootProjectDir}/hibernate-core
:coreTestSrcDir: {rootProjectDir}/hibernate-core/src/test/java
:instantiatorTestDir: {coreTestSrcDir}/org/hibernate/orm/test/mapping/embeddable/strategy/instantiator
:extrasdir: extras

Historically Hibernate called these components.
Expand Down Expand Up @@ -76,28 +80,22 @@ include::{sourcedir}/SimpleEmbeddableEquivalentTest.java[tag=embeddable-type-map

The composition form is certainly more object-oriented, and that becomes more evident as we work with multiple embeddable types.

[[embeddable-multiple]]
==== Multiple embeddable types
[[embeddable-override]]
==== Overriding Embeddable types

Although from an object-oriented perspective, it's much more convenient to work with embeddable types, this example doesn't work as-is.
When the same embeddable type is included multiple times in the same parent entity type, the Jakarta Persistence specification demands to set the associated column names explicitly.
Although from an object-oriented perspective, it's much more convenient to work with embeddable types, when we reuse the same
embeddable multiple times on the same class, the Jakarta Persistence specification requires to set the associated column names explicitly.

This requirement is due to how object properties are mapped to database columns.
By default, Jakarta Persistence expects a database column having the same name with its associated object property.
When including multiple embeddables, the implicit name-based mapping rule doesn't work anymore because multiple object properties could end-up being mapped to the same database column.
When including multiple embeddables, the implicit name-based mapping rule doesn't work anymore because multiple object
properties could end-up being mapped to the same database column.

We have a few options to handle this issue.
When an embeddable type is used multiple times, Jakarta Persistence defines the `@AttributeOverride`
and `@AssociationOverride` annotations to handle this scenario to override the default column names defined
by the Embeddable.

[[embeddable-override]]
==== Overriding Embeddable types

Jakarta Persistence defines the `@AttributeOverride` annotation to handle this scenario.
This way, the mapping conflict is resolved by setting up explicit name-based property-column type mappings.

If an Embeddable type is used multiple times in some entity, you need to use the
{jpaJavadocUrlPrefix}AttributeOverride.html[`@AttributeOverride`] and
{jpaJavadocUrlPrefix}AssociationOverride.html[`@AssociationOverride`] annotations
to override the default column names defined by the Embeddable.
NOTE: See <<embeddable-multiple-namingstrategy>> for an alternative to using `@AttributeOverride` and `@AssociationOverride`

Considering you have the following `Publisher` embeddable type
which defines a `@ManyToOne` association with the `Country` entity:
Expand Down Expand Up @@ -135,46 +133,6 @@ include::{extrasdir}/embeddable/embeddable-type-override-mapping-example.sql[]
----
====

[[embeddable-multiple-namingstrategy]]
==== Embeddables and ImplicitNamingStrategy

[IMPORTANT]
====
The `ImplicitNamingStrategyComponentPathImpl` is a Hibernate-specific feature.
Users concerned with Jakarta Persistence provider portability should instead prefer explicit column naming with `@AttributeOverride`.
====

Hibernate naming strategies are covered in detail in <<chapters/domain/naming.adoc#naming,Naming>>.
However, for the purposes of this discussion, Hibernate has the capability to interpret implicit column names in a way that is safe for use with multiple embeddable types.

[[embeddable-multiple-namingstrategy-entity-mapping]]
.Implicit multiple embeddable type mapping
====
[source,java]
----
include::{sourcedir}/EmbeddableImplicitOverrideTest.java[tag=embeddable-multiple-namingstrategy-entity-mapping, indent=0]
----
====

To make it work, you need to use the `ImplicitNamingStrategyComponentPathImpl` naming strategy.

[[embeddable-multiple-ImplicitNamingStrategyComponentPathImpl]]
.Enabling implicit embeddable type mapping using the component path naming strategy
====
[source,java]
----
include::{sourcedir}/EmbeddableImplicitOverrideTest.java[tag=embeddable-multiple-ImplicitNamingStrategyComponentPathImpl, indent=0]
----
====

Now the "path" to attributes are used in the implicit column naming:

[source,sql]
----
include::{extrasdir}/embeddable/embeddable-multiple-namingstrategy-entity-mapping.sql[]
----

You could even develop your own naming strategy to do other types of implicit naming strategies.

[[embeddable-collections]]
==== Collections of embeddable types
Expand Down Expand Up @@ -293,3 +251,81 @@ include::{sourcedir}/ParentTest.java[tags=embeddable-Parent-fetching-example]
====

Therefore, the `@Parent` annotation is used to define the association between an embeddable type and the owning entity.


[[embeddable-instantiator]]
==== Custom instantiation

Jakarta Persistence requires embeddable classes to follow Java Bean conventions. Part of this is the
definition of a non-arg constructor. However, not all value compositions applications might map as embeddable
values follow Java Bean conventions - e.g. a struct or Java 15 record.

Hibernate allows the use of a custom instantiator for creating the embeddable instances through the
`org.hibernate.metamodel.spi.EmbeddableInstantiator` contract. The custom instantiator is specified
using the `@org.hibernate.annotations.EmbeddableInstantiator` annotation. Which can either be defined
on the property:

[[embeddable-instantiator-property-ex]]
.`@EmbeddableInstantiator` on attribute
====
[source, JAVA, indent=0]
----
include::{instantiatorTestDir}/embedded/Name.java[tags=embeddable-instantiator-property]
include::{instantiatorTestDir}/embedded/Person.java[tags=embeddable-instantiator-property]
----
====

or on the embeddable class:

[[embeddable-instantiator-class-ex]]
.`@EmbeddableInstantiator` on class
====
[source, JAVA, indent=0]
----
include::{instantiatorTestDir}/embeddable/Name.java[tags=embeddable-instantiator-class]
include::{instantiatorTestDir}/embeddable/Person.java[tags=embeddable-instantiator-class]
----
====



[[embeddable-multiple-namingstrategy]]
==== Embeddables and ImplicitNamingStrategy

[IMPORTANT]
====
The `ImplicitNamingStrategyComponentPathImpl` is a Hibernate-specific feature.
Users concerned with Jakarta Persistence provider portability should instead prefer explicit column naming with `@AttributeOverride`.
====

Hibernate naming strategies are covered in detail in <<chapters/domain/naming.adoc#naming,Naming>>.
However, for the purposes of this discussion, Hibernate has the capability to interpret implicit column names in a way that is safe for use with multiple embeddable types.

[[embeddable-multiple-namingstrategy-entity-mapping]]
.Implicit multiple embeddable type mapping
====
[source,java]
----
include::{sourcedir}/EmbeddableImplicitOverrideTest.java[tag=embeddable-multiple-namingstrategy-entity-mapping, indent=0]
----
====

To make it work, you need to use the `ImplicitNamingStrategyComponentPathImpl` naming strategy.

[[embeddable-multiple-ImplicitNamingStrategyComponentPathImpl]]
.Enabling implicit embeddable type mapping using the component path naming strategy
====
[source,java]
----
include::{sourcedir}/EmbeddableImplicitOverrideTest.java[tag=embeddable-multiple-ImplicitNamingStrategyComponentPathImpl, indent=0]
----
====

Now the "path" to attributes are used in the implicit column naming:

[source,sql]
----
include::{extrasdir}/embeddable/embeddable-multiple-namingstrategy-entity-mapping.sql[]
----

You could even develop your own naming strategy to do other types of implicit naming strategies.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Allows supplying a custom instantiator implementation
*/
@Target( {TYPE, FIELD, METHOD, ANNOTATION_TYPE} )
@Retention( RUNTIME )
public @interface EmbeddableInstantiator {
Class<? extends org.hibernate.metamodel.spi.EmbeddableInstantiator> value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
import org.hibernate.mapping.Subclass;
import org.hibernate.mapping.ToOne;
import org.hibernate.mapping.UnionSubclass;
import org.hibernate.metamodel.spi.EmbeddableInstantiator;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.type.descriptor.java.BasicJavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
Expand Down Expand Up @@ -1158,6 +1159,7 @@ private static boolean mapAsIdClass(
true,
true,
false,
null,
context,
inheritanceStatePerClass
);
Expand Down Expand Up @@ -2286,6 +2288,11 @@ else if ( !isId || !entityBinder.isIgnoreIdAnnotations() ) {
}
}

if ( ! isComponent ) {
if ( property.isAnnotationPresent( Embedded.class ) ) {

}
}
isComponent = isComponent
|| property.isAnnotationPresent( Embedded.class )
|| property.isAnnotationPresent( EmbeddedId.class )
Expand All @@ -2300,7 +2307,10 @@ else if ( !isId || !entityBinder.isIgnoreIdAnnotations() ) {
);
referencedEntityName = mapsIdProperty.getClassOrElementName();
}
AccessType propertyAccessor = entityBinder.getPropertyAccessor( property );

final AccessType propertyAccessor = entityBinder.getPropertyAccessor( property );
final Class<? extends EmbeddableInstantiator> customInstantiatorImpl = determineCustomInstantiator( property, returnedClass );

propertyBinder = bindComponent(
inferredData,
propertyHolder,
Expand All @@ -2312,6 +2322,7 @@ else if ( !isId || !entityBinder.isIgnoreIdAnnotations() ) {
isId,
inheritanceStatePerClass,
referencedEntityName,
customInstantiatorImpl,
isOverridden ? ( Ejb3JoinColumn[] ) columns : null
);
}
Expand Down Expand Up @@ -2446,6 +2457,25 @@ else if ( !isId || !entityBinder.isIgnoreIdAnnotations() ) {
}
}

private static Class<? extends EmbeddableInstantiator> determineCustomInstantiator(XProperty property, XClass returnedClass) {
if ( property.isAnnotationPresent( EmbeddedId.class ) ) {
// we don't allow custom instantiators for composite ids
return null;
}

final org.hibernate.annotations.EmbeddableInstantiator propertyAnnotation = property.getAnnotation( org.hibernate.annotations.EmbeddableInstantiator.class );
if ( propertyAnnotation != null ) {
return propertyAnnotation.value();
}

final org.hibernate.annotations.EmbeddableInstantiator classAnnotation = returnedClass.getAnnotation( org.hibernate.annotations.EmbeddableInstantiator.class );
if ( classAnnotation != null ) {
return classAnnotation.value();
}

return null;
}

private static boolean isGlobalGeneratorNameGlobal(MetadataBuildingContext context) {
return context.getBootstrapContext().getJpaCompliance().isGlobalGeneratorScopeEnabled();
}
Expand Down Expand Up @@ -2659,10 +2689,11 @@ private static PropertyBinder bindComponent(
boolean isId, //is an identifier
Map<XClass, InheritanceState> inheritanceStatePerClass,
String referencedEntityName, //is a component who is overridden by a @MapsId
Class<? extends EmbeddableInstantiator> customInstantiatorImpl,
Ejb3JoinColumn[] columns) {
Component comp;
if ( referencedEntityName != null ) {
comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, buildingContext );
comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, customInstantiatorImpl, buildingContext );
SecondPass sp = new CopyIdentifierComponentSecondPass(
comp,
referencedEntityName,
Expand All @@ -2675,7 +2706,7 @@ private static PropertyBinder bindComponent(
comp = fillComponent(
propertyHolder, inferredData, propertyAccessor, !isId, entityBinder,
isComponentEmbedded, isIdentifierMapper,
false, buildingContext, inheritanceStatePerClass
false, customInstantiatorImpl, buildingContext, inheritanceStatePerClass
);
}
if ( isId ) {
Expand Down Expand Up @@ -2721,6 +2752,7 @@ public static Component fillComponent(
boolean isComponentEmbedded,
boolean isIdentifierMapper,
boolean inSecondPass,
Class<? extends EmbeddableInstantiator> customInstantiatorImpl,
MetadataBuildingContext buildingContext,
Map<XClass, InheritanceState> inheritanceStatePerClass) {
return fillComponent(
Expand All @@ -2733,6 +2765,7 @@ public static Component fillComponent(
isComponentEmbedded,
isIdentifierMapper,
inSecondPass,
customInstantiatorImpl,
buildingContext,
inheritanceStatePerClass
);
Expand All @@ -2748,14 +2781,16 @@ public static Component fillComponent(
boolean isComponentEmbedded,
boolean isIdentifierMapper,
boolean inSecondPass,
Class<? extends EmbeddableInstantiator> customInstantiatorImpl,
MetadataBuildingContext buildingContext,
Map<XClass, InheritanceState> inheritanceStatePerClass) {
/**
* inSecondPass can only be used to apply right away the second pass of a composite-element
* Because it's a value type, there is no bidirectional association, hence second pass
* ordering does not matter
*/
Component comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, buildingContext );
Component comp = createComponent( propertyHolder, inferredData, isComponentEmbedded, isIdentifierMapper, customInstantiatorImpl, buildingContext );

String subpath = BinderHelper.getPath( propertyHolder, inferredData );
LOG.tracev( "Binding component with path: {0}", subpath );
PropertyHolder subHolder = PropertyHolderBuilder.buildPropertyHolder(
Expand Down Expand Up @@ -2899,6 +2934,7 @@ public static Component createComponent(
PropertyData inferredData,
boolean isComponentEmbedded,
boolean isIdentifierMapper,
Class<? extends EmbeddableInstantiator> customInstantiatorImpl,
MetadataBuildingContext context) {
Component comp = new Component( context, propertyHolder.getPersistentClass() );
comp.setEmbedded( isComponentEmbedded );
Expand All @@ -2911,6 +2947,7 @@ public static Component createComponent(
else {
comp.setComponentClassName( inferredData.getClassOrElementName() );
}
comp.setCustomInstantiator( customInstantiatorImpl );
return comp;
}

Expand Down Expand Up @@ -2954,6 +2991,7 @@ private static void bindIdClass(
isEmbedded,
isIdentifierMapper,
false,
null,
buildingContext,
inheritanceStatePerClass
);
Expand Down

0 comments on commit 924c2b2

Please sign in to comment.