Skip to content

Commit a961ba4

Browse files
committed
Add documentation for CompositeUserType
1 parent c18e611 commit a961ba4

File tree

8 files changed

+442
-1
lines changed

8 files changed

+442
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,7 @@ There are also corresponding, specialized forms of `@Type` for specific model pa
15681568
* For other collection mappings, `@Type` describes the elements
15691569
* For discriminated association mappings (`@Any` and `@ManyToAny`), `@Type` describes the discriminator value
15701570

1571-
`@Type` allows for more complex mapping concerns; but, <<basic-jpa-convert,AttributeConverter> and
1571+
`@Type` allows for more complex mapping concerns; but, <<basic-jpa-convert,AttributeConverter>> and
15721572
<<basic-mapping-composition>> should generally be preferred as simpler solutions
15731573

15741574
[[basic-nationalized]]

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
:coreProjectDir: {rootProjectDir}/hibernate-core
66
:coreTestSrcDir: {rootProjectDir}/hibernate-core/src/test/java
77
:instantiatorTestDir: {coreTestSrcDir}/org/hibernate/orm/test/mapping/embeddable/strategy/instantiator
8+
:usertypeTestDir: {coreTestSrcDir}/org/hibernate/orm/test/mapping/embeddable/strategy/usertype
89
:extrasdir: extras
910

1011
Historically Hibernate called these components.
@@ -325,6 +326,78 @@ include::{instantiatorTestDir}/registered/Person.java[tags=embeddable-instantiat
325326

326327

327328

329+
[[embeddable-mapping-custom]]
330+
==== Custom type mapping
331+
332+
Another approach is to supply the implementation of the `org.hibernate.usertype.CompositeUserType` contract using `@CompositeType`,
333+
which is an extension to the `org.hibernate.metamodel.spi.EmbeddableInstantiator` contract.
334+
335+
There are also corresponding, specialized forms of `@CompositeType` for specific model parts:
336+
337+
* When mapping a Map, `@CompositeType` describes the Map value while `@MapKeyCompositeType` describes the Map key
338+
* For collection mappings, `@CompositeType` describes the elements
339+
340+
For example, consider the following custom type:
341+
342+
[[embeddable-usertype-domain-ex]]
343+
.`CompositeUserType` - Domain type
344+
====
345+
[source, JAVA, indent=0]
346+
----
347+
include::{usertypeTestDir}/embedded/Name.java[tags=embeddable-usertype-domain]
348+
----
349+
====
350+
351+
Here, `Name` only allows use of the constructor accepting its state. Because this class does not follow Java Bean
352+
conventions, a custom user type for instantiation and state access is needed.
353+
354+
[[embeddable-usertype-impl-ex]]
355+
.`CompositeUserType` - Implementation
356+
====
357+
[source, JAVA, indent=0]
358+
----
359+
include::{usertypeTestDir}/embedded/NameCompositeUserType.java[tags=embeddable-usertype-impl]
360+
----
361+
====
362+
363+
A composite user type needs an embeddable mapper class, which represents the embeddable mapping structure of the type
364+
i.e. the way the type would look like if you had the option to write a custom `@Embeddable` class.
365+
366+
In addition to the instantiation logic, a composite user type also has to provide a way to decompose the returned type
367+
into the individual components/properties of the embeddable mapper class through `getPropertyValue`.
368+
The property index, just like in the `instantiate` method, is based on the alphabetical order of the attribute names
369+
of the embeddable mapper class.
370+
371+
The composite user type also needs to provide methods to handle the mutability, equals, hashCode and the cache
372+
serialization and deserialization of the returned type.
373+
374+
There are a few ways to specify the composite user type. The `@org.hibernate.annotations.CompositeType`
375+
annotation can be used on the embedded and element collection attributes:
376+
377+
[[embeddable-usertype-property-ex]]
378+
.`@CompositeType` on attribute
379+
====
380+
[source, JAVA, indent=0]
381+
----
382+
include::{usertypeTestDir}/embedded/Person.java[tags=embeddable-usertype-property]
383+
----
384+
====
385+
386+
Or `@org.hibernate.annotations.CompositeTypeRegistration` may be used, which is useful
387+
when the application developer wants to apply the composite user type for all domain type uses.
388+
389+
[[embeddable-usertype-registration-ex]]
390+
.`@CompositeTypeRegistration`
391+
====
392+
[source, JAVA, indent=0]
393+
----
394+
include::{usertypeTestDir}/registered/Person.java[tags=embeddable-usertype-registration]
395+
----
396+
====
397+
398+
399+
400+
328401
[[embeddable-multiple-namingstrategy]]
329402
==== Embeddables and ImplicitNamingStrategy
330403

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded;
2+
3+
import org.hibernate.testing.orm.junit.DomainModel;
4+
import org.hibernate.testing.orm.junit.SessionFactory;
5+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
@DomainModel( annotatedClasses = { Person.class, Name.class } )
11+
@SessionFactory
12+
public class CompositeUserTypeTests {
13+
14+
@Test
15+
public void basicTest(SessionFactoryScope scope) {
16+
scope.inTransaction( (session) -> {
17+
final Person mick = new Person( 1, new Name( "Mick", "Jagger" ) );
18+
session.persist( mick );
19+
20+
final Person john = new Person( 2, new Name( "John", "Doe" ) );
21+
john.addAlias( new Name( "Jon", "Doe" ) );
22+
session.persist( john );
23+
} );
24+
scope.inTransaction( (session) -> {
25+
final Person mick = session.createQuery( "from Person where id = 1", Person.class ).uniqueResult();
26+
assertThat( mick.getName().firstName() ).isEqualTo( "Mick" );
27+
} );
28+
scope.inTransaction( (session) -> {
29+
final Person john = session.createQuery( "from Person p join fetch p.aliases where p.id = 2", Person.class ).uniqueResult();
30+
assertThat( john.getName().firstName() ).isEqualTo( "John" );
31+
assertThat( john.getAliases() ).hasSize( 1 );
32+
final Name alias = john.getAliases().iterator().next();
33+
assertThat( alias.firstName() ).isEqualTo( "Jon" );
34+
} );
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
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
6+
*/
7+
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded;
8+
9+
//tag::embeddable-usertype-domain[]
10+
public class Name {
11+
private final String first;
12+
private final String last;
13+
14+
public Name(String first, String last) {
15+
this.first = first;
16+
this.last = last;
17+
}
18+
19+
public String firstName() {
20+
return first;
21+
}
22+
23+
public String lastName() {
24+
return last;
25+
}
26+
}
27+
//end::embeddable-usertype-domain[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
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
6+
*/
7+
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded;
8+
9+
import java.io.Serializable;
10+
import java.util.Objects;
11+
12+
import org.hibernate.HibernateException;
13+
import org.hibernate.engine.spi.SessionFactoryImplementor;
14+
import org.hibernate.metamodel.spi.ValueAccess;
15+
import org.hibernate.usertype.CompositeUserType;
16+
17+
//tag::embeddable-usertype-impl[]
18+
public class NameCompositeUserType implements CompositeUserType<Name> {
19+
20+
public static class NameMapper {
21+
String firstName;
22+
String lastName;
23+
}
24+
25+
@Override
26+
public Class<?> embeddable() {
27+
return NameMapper.class;
28+
}
29+
30+
@Override
31+
public Class<Name> returnedClass() {
32+
return Name.class;
33+
}
34+
35+
@Override
36+
public Name instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
37+
// alphabetical
38+
final String first = valueAccess.getValue( 0, String.class );
39+
final String last = valueAccess.getValue( 1, String.class );
40+
return new Name( first, last );
41+
}
42+
43+
@Override
44+
public Object getPropertyValue(Name component, int property) throws HibernateException {
45+
// alphabetical
46+
switch ( property ) {
47+
case 0:
48+
return component.firstName();
49+
case 1:
50+
return component.lastName();
51+
}
52+
return null;
53+
}
54+
55+
@Override
56+
public boolean equals(Name x, Name y) {
57+
return x == y || x != null && Objects.equals( x.firstName(), y.firstName() )
58+
&& Objects.equals( x.lastName(), y.lastName() );
59+
}
60+
61+
@Override
62+
public int hashCode(Name x) {
63+
return Objects.hash( x.firstName(), x.lastName() );
64+
}
65+
66+
@Override
67+
public Name deepCopy(Name value) {
68+
return value; // immutable
69+
}
70+
71+
@Override
72+
public boolean isMutable() {
73+
return false;
74+
}
75+
76+
@Override
77+
public Serializable disassemble(Name value) {
78+
return new String[] { value.firstName(), value.lastName() };
79+
}
80+
81+
@Override
82+
public Name assemble(Serializable cached, Object owner) {
83+
final String[] parts = (String[]) cached;
84+
return new Name( parts[0], parts[1] );
85+
}
86+
87+
@Override
88+
public Name replace(Name detached, Name managed, Object owner) {
89+
return detached;
90+
}
91+
92+
}
93+
//end::embeddable-usertype-impl[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
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
6+
*/
7+
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded;
8+
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
12+
import org.hibernate.annotations.CompositeType;
13+
14+
import jakarta.persistence.AttributeOverride;
15+
import jakarta.persistence.Column;
16+
import jakarta.persistence.ElementCollection;
17+
import jakarta.persistence.Embedded;
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.Id;
20+
import jakarta.persistence.Table;
21+
22+
23+
@Table(name = "people")
24+
//tag::embeddable-usertype-property[]
25+
@Entity
26+
public class Person {
27+
@Id
28+
public Integer id;
29+
30+
@Embedded
31+
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
32+
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
33+
@CompositeType( NameCompositeUserType.class )
34+
public Name name;
35+
36+
@ElementCollection
37+
@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
38+
@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
39+
@CompositeType( NameCompositeUserType.class )
40+
public Set<Name> aliases;
41+
42+
//end::embeddable-usertype-property[]
43+
44+
private Person() {
45+
// for Hibernate use
46+
}
47+
48+
public Person(Integer id, Name name) {
49+
this.id = id;
50+
this.name = name;
51+
}
52+
53+
public Integer getId() {
54+
return id;
55+
}
56+
57+
public Name getName() {
58+
return name;
59+
}
60+
61+
public void setName(Name name) {
62+
this.name = name;
63+
}
64+
65+
public void setId(Integer id) {
66+
this.id = id;
67+
}
68+
69+
public Set<Name> getAliases() {
70+
return aliases;
71+
}
72+
73+
public void setAliases(Set<Name> aliases) {
74+
this.aliases = aliases;
75+
}
76+
77+
public void addAlias(Name alias) {
78+
if ( aliases == null ) {
79+
aliases = new HashSet<>();
80+
}
81+
aliases.add( alias );
82+
}
83+
84+
//tag::embeddable-usertype-property[]
85+
}
86+
//end::embeddable-usertype-property[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.hibernate.orm.test.mapping.embeddable.strategy.usertype.registered;
2+
3+
import org.hibernate.orm.test.mapping.embeddable.strategy.usertype.embedded.Name;
4+
5+
import org.hibernate.testing.orm.junit.DomainModel;
6+
import org.hibernate.testing.orm.junit.SessionFactory;
7+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
@DomainModel( annotatedClasses = { Person.class, Name.class } )
13+
@SessionFactory
14+
public class CompositeUserTypeTests {
15+
16+
@Test
17+
public void basicTest(SessionFactoryScope scope) {
18+
scope.inTransaction( (session) -> {
19+
final Person mick = new Person( 1, new Name( "Mick", "Jagger" ) );
20+
session.persist( mick );
21+
22+
final Person john = new Person( 2, new Name( "John", "Doe" ) );
23+
john.addAlias( new Name( "Jon", "Doe" ) );
24+
session.persist( john );
25+
} );
26+
scope.inTransaction( (session) -> {
27+
final Person mick = session.createQuery( "from Person where id = 1", Person.class ).uniqueResult();
28+
assertThat( mick.getName().firstName() ).isEqualTo( "Mick" );
29+
} );
30+
scope.inTransaction( (session) -> {
31+
final Person john = session.createQuery( "from Person p join fetch p.aliases where p.id = 2", Person.class ).uniqueResult();
32+
assertThat( john.getName().firstName() ).isEqualTo( "John" );
33+
assertThat( john.getAliases() ).hasSize( 1 );
34+
final Name alias = john.getAliases().iterator().next();
35+
assertThat( alias.firstName() ).isEqualTo( "Jon" );
36+
} );
37+
}
38+
}

0 commit comments

Comments
 (0)