Add documentation for CompositeUserType

This commit is contained in:
Christian Beikov 2022-05-04 12:57:51 +02:00
parent c18e611ed6
commit a961ba45ac
8 changed files with 442 additions and 1 deletions

View File

@ -1568,7 +1568,7 @@ There are also corresponding, specialized forms of `@Type` for specific model pa
* For other collection mappings, `@Type` describes the elements
* For discriminated association mappings (`@Any` and `@ManyToAny`), `@Type` describes the discriminator value
`@Type` allows for more complex mapping concerns; but, <<basic-jpa-convert,AttributeConverter> and
`@Type` allows for more complex mapping concerns; but, <<basic-jpa-convert,AttributeConverter>> and
<<basic-mapping-composition>> should generally be preferred as simpler solutions
[[basic-nationalized]]

View File

@ -5,6 +5,7 @@
:coreProjectDir: {rootProjectDir}/hibernate-core
:coreTestSrcDir: {rootProjectDir}/hibernate-core/src/test/java
:instantiatorTestDir: {coreTestSrcDir}/org/hibernate/orm/test/mapping/embeddable/strategy/instantiator
:usertypeTestDir: {coreTestSrcDir}/org/hibernate/orm/test/mapping/embeddable/strategy/usertype
:extrasdir: extras
Historically Hibernate called these components.
@ -325,6 +326,78 @@ include::{instantiatorTestDir}/registered/Person.java[tags=embeddable-instantiat
[[embeddable-mapping-custom]]
==== Custom type mapping
Another approach is to supply the implementation of the `org.hibernate.usertype.CompositeUserType` contract using `@CompositeType`,
which is an extension to the `org.hibernate.metamodel.spi.EmbeddableInstantiator` contract.
There are also corresponding, specialized forms of `@CompositeType` for specific model parts:
* When mapping a Map, `@CompositeType` describes the Map value while `@MapKeyCompositeType` describes the Map key
* For collection mappings, `@CompositeType` describes the elements
For example, consider the following custom type:
[[embeddable-usertype-domain-ex]]
.`CompositeUserType` - Domain type
====
[source, JAVA, indent=0]
----
include::{usertypeTestDir}/embedded/Name.java[tags=embeddable-usertype-domain]
----
====
Here, `Name` only allows use of the constructor accepting its state. Because this class does not follow Java Bean
conventions, a custom user type for instantiation and state access is needed.
[[embeddable-usertype-impl-ex]]
.`CompositeUserType` - Implementation
====
[source, JAVA, indent=0]
----
include::{usertypeTestDir}/embedded/NameCompositeUserType.java[tags=embeddable-usertype-impl]
----
====
A composite user type needs an embeddable mapper class, which represents the embeddable mapping structure of the type
i.e. the way the type would look like if you had the option to write a custom `@Embeddable` class.
In addition to the instantiation logic, a composite user type also has to provide a way to decompose the returned type
into the individual components/properties of the embeddable mapper class through `getPropertyValue`.
The property index, just like in the `instantiate` method, is based on the alphabetical order of the attribute names
of the embeddable mapper class.
The composite user type also needs to provide methods to handle the mutability, equals, hashCode and the cache
serialization and deserialization of the returned type.
There are a few ways to specify the composite user type. The `@org.hibernate.annotations.CompositeType`
annotation can be used on the embedded and element collection attributes:
[[embeddable-usertype-property-ex]]
.`@CompositeType` on attribute
====
[source, JAVA, indent=0]
----
include::{usertypeTestDir}/embedded/Person.java[tags=embeddable-usertype-property]
----
====
Or `@org.hibernate.annotations.CompositeTypeRegistration` may be used, which is useful
when the application developer wants to apply the composite user type for all domain type uses.
[[embeddable-usertype-registration-ex]]
.`@CompositeTypeRegistration`
====
[source, JAVA, indent=0]
----
include::{usertypeTestDir}/registered/Person.java[tags=embeddable-usertype-registration]
----
====
[[embeddable-multiple-namingstrategy]]
==== Embeddables and ImplicitNamingStrategy

View File

@ -0,0 +1,36 @@
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@DomainModel( annotatedClasses = { Person.class, Name.class } )
@SessionFactory
public class CompositeUserTypeTests {
@Test
public void basicTest(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Person mick = new Person( 1, new Name( "Mick", "Jagger" ) );
session.persist( mick );
final Person john = new Person( 2, new Name( "John", "Doe" ) );
john.addAlias( new Name( "Jon", "Doe" ) );
session.persist( john );
} );
scope.inTransaction( (session) -> {
final Person mick = session.createQuery( "from Person where id = 1", Person.class ).uniqueResult();
assertThat( mick.getName().firstName() ).isEqualTo( "Mick" );
} );
scope.inTransaction( (session) -> {
final Person john = session.createQuery( "from Person p join fetch p.aliases where p.id = 2", Person.class ).uniqueResult();
assertThat( john.getName().firstName() ).isEqualTo( "John" );
assertThat( john.getAliases() ).hasSize( 1 );
final Name alias = john.getAliases().iterator().next();
assertThat( alias.firstName() ).isEqualTo( "Jon" );
} );
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.orm.test.mapping.embeddable.strategy.usertype.embedded;
//tag::embeddable-usertype-domain[]
public class Name {
private final String first;
private final String last;
public Name(String first, String last) {
this.first = first;
this.last = last;
}
public String firstName() {
return first;
}
public String lastName() {
return last;
}
}
//end::embeddable-usertype-domain[]

View File

@ -0,0 +1,93 @@
/*
* 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.orm.test.mapping.embeddable.strategy.usertype.embedded;
import java.io.Serializable;
import java.util.Objects;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.spi.ValueAccess;
import org.hibernate.usertype.CompositeUserType;
//tag::embeddable-usertype-impl[]
public class NameCompositeUserType implements CompositeUserType<Name> {
public static class NameMapper {
String firstName;
String lastName;
}
@Override
public Class<?> embeddable() {
return NameMapper.class;
}
@Override
public Class<Name> returnedClass() {
return Name.class;
}
@Override
public Name instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
// alphabetical
final String first = valueAccess.getValue( 0, String.class );
final String last = valueAccess.getValue( 1, String.class );
return new Name( first, last );
}
@Override
public Object getPropertyValue(Name component, int property) throws HibernateException {
// alphabetical
switch ( property ) {
case 0:
return component.firstName();
case 1:
return component.lastName();
}
return null;
}
@Override
public boolean equals(Name x, Name y) {
return x == y || x != null && Objects.equals( x.firstName(), y.firstName() )
&& Objects.equals( x.lastName(), y.lastName() );
}
@Override
public int hashCode(Name x) {
return Objects.hash( x.firstName(), x.lastName() );
}
@Override
public Name deepCopy(Name value) {
return value; // immutable
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Name value) {
return new String[] { value.firstName(), value.lastName() };
}
@Override
public Name assemble(Serializable cached, Object owner) {
final String[] parts = (String[]) cached;
return new Name( parts[0], parts[1] );
}
@Override
public Name replace(Name detached, Name managed, Object owner) {
return detached;
}
}
//end::embeddable-usertype-impl[]

View File

@ -0,0 +1,86 @@
/*
* 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.orm.test.mapping.embeddable.strategy.usertype.embedded;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.annotations.CompositeType;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Table(name = "people")
//tag::embeddable-usertype-property[]
@Entity
public class Person {
@Id
public Integer id;
@Embedded
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
@CompositeType( NameCompositeUserType.class )
public Name name;
@ElementCollection
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
@CompositeType( NameCompositeUserType.class )
public Set<Name> aliases;
//end::embeddable-usertype-property[]
private Person() {
// for Hibernate use
}
public Person(Integer id, Name name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
public void setId(Integer id) {
this.id = id;
}
public Set<Name> getAliases() {
return aliases;
}
public void setAliases(Set<Name> aliases) {
this.aliases = aliases;
}
public void addAlias(Name alias) {
if ( aliases == null ) {
aliases = new HashSet<>();
}
aliases.add( alias );
}
//tag::embeddable-usertype-property[]
}
//end::embeddable-usertype-property[]

View File

@ -0,0 +1,38 @@
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.registered;
import org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded.Name;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@DomainModel( annotatedClasses = { Person.class, Name.class } )
@SessionFactory
public class CompositeUserTypeTests {
@Test
public void basicTest(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Person mick = new Person( 1, new Name( "Mick", "Jagger" ) );
session.persist( mick );
final Person john = new Person( 2, new Name( "John", "Doe" ) );
john.addAlias( new Name( "Jon", "Doe" ) );
session.persist( john );
} );
scope.inTransaction( (session) -> {
final Person mick = session.createQuery( "from Person where id = 1", Person.class ).uniqueResult();
assertThat( mick.getName().firstName() ).isEqualTo( "Mick" );
} );
scope.inTransaction( (session) -> {
final Person john = session.createQuery( "from Person p join fetch p.aliases where p.id = 2", Person.class ).uniqueResult();
assertThat( john.getName().firstName() ).isEqualTo( "John" );
assertThat( john.getAliases() ).hasSize( 1 );
final Name alias = john.getAliases().iterator().next();
assertThat( alias.firstName() ).isEqualTo( "Jon" );
} );
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.orm.test.mapping.embeddable.strategy.usertype.registered;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.annotations.CompositeType;
import org.hibernate.annotations.CompositeTypeRegistration;
import org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded.Name;
import org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded.NameCompositeUserType;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Table(name = "people")
//tag::embeddable-usertype-registration[]
@Entity
@CompositeTypeRegistration( embeddableClass = Name.class, userType = NameCompositeUserType.class )
public class Person {
@Id
public Integer id;
@Embedded
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
public Name name;
@ElementCollection
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
public Set<Name> aliases;
//end::embeddable-usertype-registration[]
private Person() {
// for Hibernate use
}
public Person(Integer id, Name name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
public void setId(Integer id) {
this.id = id;
}
public Set<Name> getAliases() {
return aliases;
}
public void setAliases(Set<Name> aliases) {
this.aliases = aliases;
}
public void addAlias(Name alias) {
if ( aliases == null ) {
aliases = new HashSet<>();
}
aliases.add( alias );
}
//tag::embeddable-usertype-registration[]
}
//end::embeddable-usertype-registration[]