User Guide - collection mapping docs

This commit is contained in:
Steve Ebersole 2022-01-24 19:52:22 -06:00
parent 610446270e
commit 3f879dc3db
6 changed files with 246 additions and 152 deletions

View File

@ -1538,7 +1538,7 @@ There are also corresponding, specialized forms of `@Type` for specific model pa
* When mapping a Map, `@Type` describes the Map value while `@MapKeyCustomType` describe the Map key
* When mapping a List or array, `@Type` describes the elements while `@ListIndexCustomType` describes the index
* When mapping an id-bag, `@Type` describes the elements while `@CollectionIdCustomType` describes the collection-id
* When mapping an id-bag, `@Type` describes the elements while `CollectionIdType` describes the collection-id
* For other collection mappings, `@Type` describes the elements
* For discriminated association mappings (`@Any` and `@ManyToAny`), `@Type` describes the discriminator value

View File

@ -15,49 +15,48 @@
Hibernate supports mapping collections (`java.util.Collection` and `java.util.Map` subtypes)
in a variety of ways.
TIP:: Hibernate even allows mapping a collection as `@Basic`, but that should generally be avoided.
Hibernate even allows mapping a collection as `@Basic`, but that should generally be avoided.
See <<collections-as-basic>> for details of such a mapping.
This section is limited to discussing `@ElementCollection`, `@OneToMany` and `@ManyToMany`.
Hibernate understands various aspects of collection mappings:
Semantics:: the type of collection (`List`, `Set`, etc.) and how Hibernate deals with it as a whole - <<collection-semantics>>
Parts:: the individual parts of the collection (element, e.g.) that map to the database - <<collection-parts>>
Nature:: whether the collection parts are considered elemental (`@ElementCollection`) or associative (`@OneToMany` and `@ManyToMany`) - <<collection-nature>>
[IMPORTANT]
====
Two entities cannot share a reference to the same collection instance.
IMPORTANT:: Collections mapped by Hibernate cannot be nested (`List<List<?>>`), When used in collections, embeddable types cannot define plural attributes.
Collection-valued properties do not support null value semantics.
Hibernate provides its own implementation of the collection types (plus array mappings) for various
purposes. See <<collection-wrapper>> for details.
Collections cannot be nested, meaning Hibernate does not support mapping `List<List<?>>`, for example.
[[collection-semantics]]
==== Semantics
Embeddables which are used as a collection element, Map value or Map key may not themselves define collections
====
The semantics of a collection include the specific collection subtype to use as well
as how to handle the collection as a whole. This breaks down based on the classification
as represented by `org.hibernate.metamodel.CollectionClassification`, which indicates the
`org.hibernate.collection.spi.CollectionSemantics` to use. `CollectionSemantics` exposes
the capabilities of the mapping such as
[[collections-semantics]]
==== Collection Semantics
The semantics of a collection describes how to handle the collection, including
* the collection subtype to use - `java.util.List`, `java.util.Set`, `java.util.SortedSet`, etc.
* how to access elements of the collection
* how to create instances of the collection - both "raw" and "wrapper" forms. See <<collection-wrapper>>
Hibernate supports the following classifications:
Hibernate supports the following semantics:
ARRAY:: Object and primitive arrays. See <<collection-array>>.
BAG:: A collection that may contain duplicate entries and has no defined ordering. See <<collection-bag>>.
ID_BAG:: A bag that defines a per-element identifier to uniquely identify elements in the collection. See <<collection-bag>>.
LIST:: Follows the semantics defined by `java.util.List`. See <<collection-list>>.
SET:: Follows the semantics defined by `java.util.Set`. See <<collection-set>>.
ORDERED_SET:: A set that is ordered by a SQL fragment defined on its mapping. See <<collection-set>>.
SORTED_SET:: A set that is sorted according to a `Comparator` defined on its mapping. See <<collection-set>>.
MAP:: Follows the semantics defined by `java.util.Map`. See <<collection-map>>.
ORDERED_MAP:: A map that is ordered by keys according to a SQL fragment defined on its mapping. See <<collection-map>>.
SORTED_MAP:: A map that is sorted by keys according to a `Comparator` defined on its mapping. See <<collection-map>>.
ARRAY:: Object and primitive arrays. See <<collections-array>>.
BAG:: A collection that may contain duplicate entries and has no defined ordering. See <<collections-bag>>.
ID_BAG:: A bag that defines a per-element identifier to uniquely identify elements in the collection. See <<collections-bag>>.
LIST:: Follows the semantics defined by `java.util.List`. See <<collections-list>>.
SET:: Follows the semantics defined by `java.util.Set`. See <<collections-set>>.
ORDERED_SET:: A set that is ordered by a SQL fragment defined on its mapping. See <<collections-set>>.
SORTED_SET:: A set that is sorted according to a `Comparator` defined on its mapping. See <<collections-set>>.
MAP:: Follows the semantics defined by `java.util.Map`. See <<collections-map>>.
ORDERED_MAP:: A map that is ordered by keys according to a SQL fragment defined on its mapping. See <<collections-map>>.
SORTED_MAP:: A map that is sorted by keys according to a `Comparator` defined on its mapping. See <<collections-map>>.
By default, Hiberate interprets the defined type of the plural attribute and makes an
By default, Hibernate interprets the defined type of the plural attribute and makes an
interpretation as to which classification it fits in to, using the following checks:
1. if an array -> ARRAY
@ -69,81 +68,6 @@ interpretation as to which classification it fits in to, using the following che
7. else `Collection` -> BAG
The following sections related to mapping the various classifications all use simple
"element collections" for their examples. Element collections versus association
collections are discussed later in <<collection-nature>>. The actual classification
configuration is the same between them. The examples map collections of a `Name` type:
[[collection-name-ex]]
.Name class
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/Name.java[tags=collections-name-ex]
----
====
[[collection-set]]
==== Mapping Sets
`java.util.Set` defines a collection of unique, though unordered elements. Hibernate supports
mapping sets according to the requirements of the `java.util.Set`.
[[collection-set-ex]]
.Basic Set Mapping
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/set/EntityWithSet.java[tags=collections-set-ex]
----
====
Hibernate also has the ability to map sorted and ordered sets. A sorted set orders its
elements in memory via an associated `Comparator`; an ordered set is ordered via
SQL when the set is loaded.
TIP:: An ordered set does not perform any sorting in-memory. If an element is added
after the collection is loaded, the collection would need to be refreshed to re-order
the elements. For this reason, ordered sets are not recommended - if the application
needs ordering of the set elements, a sorted set should be preferred. For this reason,
it is not covered in the User Guide. See the javadocs for `jakarta.persistence.OrderBy`
or `org.hibernate.annotations.OrderBy` for details.
There are 2 options for sorting a set - naturally or using an explicit comparator.
A set is naturally sorted using the natural sort comparator for its elements. Generally
this implies that the element type is `Comparable`. E.g.
[[collection-sortedset-natural-ex]]
.@SortNatural
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/set/EntityWithNaturallySortedSet.java[tags=collections-sortedset-natural-ex]
----
====
Because `Name` is defined as `Comparable`, its `#compare` method will be used to sort the elements in this
set.
But Hibernate also allows sorting based on a specific `Comparator` implementation. Here, e.g., we map
the `Names` as sorted by a `NameComparator`:
[[collection-sortedset-comparator-ex]]
.@SortComparator
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/NameComparator.java[tags=collections-name-comparator-ex]
include::{classificationTestsDir}/set/EntityWithSortedSet.java[tags=collections-sortedset-comparator-ex]
----
====
Here, instead of `Name#compare` being use for the sorting, the explicit `NameComparator` will be used
instead.
[[collection-list]]
==== Mapping Lists
@ -188,7 +112,7 @@ include::{classificationTestsDir}/list/EntityWithOrderColumnList.java[tags=colle
Now, a column named `name_index` will be used.
Hibernate stores index values into the order-column based on the element's position in the list
with no adjustment. The element at `names[0]` is stored with `name_position=0` and so on. That is to say
with no adjustment. The element at `names[0]` is stored with `name_index=0` and so on. That is to say
that the list index is considered 0-based just as list indexes themselves are 0-based. Some legacy
schemas might map the position as 1-based, or any base really. Hibernate also defines support for such
cases using its `@ListIndexBase` annotation.
@ -204,13 +128,89 @@ include::{classificationTestsDir}/list/EntityWithIndexBasedList.java[tags=collec
[[collection-map]]
[[collections-set]]
==== Mapping Sets
`java.util.Set` defines a collection of unique, though unordered elements. Hibernate supports
mapping sets according to the requirements of the `java.util.Set`.
[[collection-set-ex]]
.Basic Set Mapping
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/set/EntityWithSet.java[tags=collections-set-ex]
----
====
Hibernate also has the ability to map sorted and ordered sets. A sorted set orders its
elements in memory via an associated `Comparator`; an ordered set is ordered via
SQL when the set is loaded.
TIP:: An ordered set does not perform any sorting in-memory. If an element is added
after the collection is loaded, the collection would need to be refreshed to re-order
the elements. For this reason, ordered sets are not recommended - if the application
needs ordering of the set elements, a sorted set should be preferred. For this reason,
it is not covered in the User Guide. See the javadocs for `jakarta.persistence.OrderBy`
or `org.hibernate.annotations.OrderBy` for details.
There are 2 options for sorting a set - naturally or using an explicit comparator.
A set is naturally sorted using the natural sort comparator for its elements. Generally
this implies that the element type is `Comparable`. E.g.
[[collection-sortedset-natural-ex]]
.@SortNatural
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/Name.java[tags=collections-name-ex]
include::{classificationTestsDir}/set/EntityWithNaturallySortedSet.java[tags=collections-sortedset-natural-ex]
----
====
Because `Name` is defined as `Comparable`, its `#compare` method will be used to sort the elements in this
set.
But Hibernate also allows sorting based on a specific `Comparator` implementation. Here, e.g., we map
the `Names` as sorted by a `NameComparator`:
[[collection-sortedset-comparator-ex]]
.@SortComparator
====
[source, JAVA, indent=0]
----
include::{classificationTestsDir}/NameComparator.java[tags=collections-name-comparator-ex]
include::{classificationTestsDir}/set/EntityWithSortedSet.java[tags=collections-sortedset-comparator-ex]
----
====
Here, instead of `Name#compare` being use for the sorting, the explicit `NameComparator` will be used
instead.
[[collections-map]]
==== Mapping Maps
// todo (6.0) - finish
[[collection-bag]]
[[collections-bag]]
==== Mapping Collections
Without any other mapping influencers, `java.util.Collection` is interpreted using BAG
@ -259,7 +259,9 @@ table. `@CollectionId` is the annotation to configure this identifier
// todo (6.0) - finish
[[collection-array]]
[[collections-array]]
==== Mapping Arrays
Hibernate is able to map Object and primitive arrays as collections. Mapping an array is essentially
@ -274,36 +276,17 @@ these would be mapped as binary data.
[[collection-nature]]
==== Nature
The elements of a collection may either be value types (basic or embedded) or associations to
entities.
Collections of basic- or embedded-valued elements are called element collections. They are mapped
using the `jakarta.persistence.ElementCollection` annotation.
Collections of entity-valued elements are called association collections. They are mapped using
either the `jakarta.persistence.OneToMany` or `jakarta.persistence.ManyToMany` annotations.
[IMPORTANT]
====
Two entities cannot share a reference to the same collection instance.
Collection-valued properties do not support null value semantics because Hibernate does not distinguish between a null collection reference and an empty collection.
====
// todo (6.0) - finish
[[collection-elemental]]
===== @ElementCollection
[[collections-elemental]]
==== @ElementCollection
Element collections may contain values of either basic or embeddable types. They have a similar
lifecycle to basic/embedded attributes in that their persistence is completely managed as part of
the owner - they are created when referenced from an owner and automatically deleted when
unreferenced. The specifics of how this lifecycle manifests in terms of database calls depends
on the <<collection-semantics,semantics>> of the mapping.
on the <<collections-semantics,semantics>> of the mapping.
This section will discuss these lifecycle aspects using the example of mapping a collection
of phone numbers. The examples use embeddable values, but the same aspects apply to collections
@ -348,6 +331,14 @@ include::{extrasdir}/elemental-bag-lifecycle-example.sql[]
====
// todo (6.0) - finish
[[collection-nature-entity]]
@ -367,17 +358,8 @@ A bidirectional association has an _owning_ side and an _inverse (mappedBy)_ sid
// todo (6.0) - finish
[[collection-parts]]
==== Parts
A collection mapping includes several parts:
Key:: The foreign-key that associates the collection with it owner
Element:: The element of the collection (or value of a Map)
Index:: the index of the collection/array (or key of a Map)
Id:: the identifier for an ID_BAG
// todo (6.0) - finish
@ -426,22 +408,30 @@ include::{coreCollectionTestsDir}/semantics/UniqueListWrapper.java[tags=collecti
`UniqueListWrapper` is the `PersistentCollection` implementation for the "unique list" semantic. See <<collection-wrapper>> for more details.
For cases where an application wants to apply the same custom type to all
plural attributes of a given classification, Hibernate also provides the
`@CollectionTypeRegistration`:
// todo (6.0) - example of using `@CollectionTypeRegistration`
[[collection-type-reg-ann]]
==== @CollectionTypeRegistration
For cases where an application wants to apply the same custom type to all
plural attributes of a given classification, Hibernate also provides the
`@CollectionTypeRegistration`:
[[collection-type-usertype-registration-ex]]
.UniqueListType Registration
====
[source, JAVA, indent=0]
----
include::{coreCollectionTestsDir}/semantics/TheEntityWithUniqueListRegistration.java[tags=ex-collections-custom-type-model]
----
====
This example behaves exactly as in <<collection-type-ann-ex>>.
[[collection-wrapper]]
==== Wrappers
As mentioned in <<collection-semantics>>, Hibernate provides its own implementations
As mentioned in <<collections-semantics>>, Hibernate provides its own implementations
of the Java collection types. These are called wrappers as they wrap an underlying
collection and provide support for things like lazy loading, queueing add/remove
operations while detached, etc. Hibernate defines the following `PersistentCollection`
@ -462,8 +452,6 @@ The collections they wrap are called "raw" collections, which are generally the
Java implementations (`java.util.ArrayList`, etc)
// todo (6.0) - finish

View File

@ -21,7 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
*/
@java.lang.annotation.Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CollectionIdCustomType {
public @interface CollectionIdType {
/**
* The custom type implementor class

View File

@ -26,7 +26,7 @@ import org.hibernate.annotations.AnyKeyJavaType;
import org.hibernate.annotations.AnyKeyJdbcType;
import org.hibernate.annotations.AnyKeyJdbcTypeCode;
import org.hibernate.annotations.CollectionId;
import org.hibernate.annotations.CollectionIdCustomType;
import org.hibernate.annotations.CollectionIdType;
import org.hibernate.annotations.CollectionIdJavaType;
import org.hibernate.annotations.CollectionIdJdbcType;
import org.hibernate.annotations.CollectionIdJdbcTypeCode;
@ -1519,7 +1519,7 @@ public class BasicValueBinder<T> implements JdbcTypeIndicators {
@Override
public Class<? extends UserType<?>> customType(XProperty xProperty) {
final CollectionIdCustomType customType = findAnnotation( xProperty, CollectionIdCustomType.class );
final CollectionIdType customType = findAnnotation( xProperty, CollectionIdType.class );
if ( customType == null ) {
return null;
}
@ -1529,7 +1529,7 @@ public class BasicValueBinder<T> implements JdbcTypeIndicators {
@Override
public Parameter[] customTypeParameters(XProperty xProperty) {
final CollectionIdCustomType customType = findAnnotation( xProperty, CollectionIdCustomType.class );
final CollectionIdType customType = findAnnotation( xProperty, CollectionIdType.class );
if ( customType == null ) {
return null;
}

View File

@ -25,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.fail;
/**
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = TheEntityWithUniqueList.class )
@DomainModel( annotatedClasses = { TheEntityWithUniqueList.class, TheEntityWithUniqueListRegistration.class } )
@SessionFactory
public class CustomSemanticsTest {
@ -40,6 +40,16 @@ public class CustomSemanticsTest {
final CustomCollectionType collectionType = (CustomCollectionType) semantics.getCollectionType();
assertThat( collectionType.getUserType() ).isInstanceOf( UniqueListType.class );
} );
scope.withHierarchy( TheEntityWithUniqueListRegistration.class, (entityDescriptor) -> {
final Property strings = entityDescriptor.getProperty( "strings" );
final org.hibernate.mapping.Collection collectionDescriptor = (org.hibernate.mapping.Collection) strings.getValue();
assertThat( collectionDescriptor.getCollectionSemantics() ).isInstanceOf( CustomCollectionTypeSemantics.class );
final CustomCollectionTypeSemantics semantics = (CustomCollectionTypeSemantics) collectionDescriptor.getCollectionSemantics();
assertThat( semantics.getCollectionType() ).isInstanceOf( CustomCollectionType.class );
final CustomCollectionType collectionType = (CustomCollectionType) semantics.getCollectionType();
assertThat( collectionType.getUserType() ).isInstanceOf( UniqueListType.class );
} );
}
@Test
@ -65,6 +75,29 @@ public class CustomSemanticsTest {
} );
}
@Test
public void testBasicRegistrationUsage(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final TheEntityWithUniqueListRegistration entity = new TheEntityWithUniqueListRegistration( 1, "first" );
entity.setStrings( new ArrayList<>() );
entity.getStrings().add( "the string" );
entity.getStrings().add( "another" );
session.persist( entity );
} );
scope.inTransaction( (session) -> {
final TheEntityWithUniqueListRegistration loaded = session.createQuery( "from TheEntityWithUniqueListRegistration", TheEntityWithUniqueListRegistration.class ).uniqueResult();
// try to re-add one, should throw IllegalArgumentException
try {
loaded.getStrings().add( "another" );
fail( "Expecting IllegalArgumentException" );
}
catch (IllegalArgumentException expected) {
// expected outcome
}
} );
}
@AfterEach
public void cleanupTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {

View File

@ -0,0 +1,73 @@
/*
* 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.collections.semantics;
import java.util.List;
import jakarta.persistence.Basic;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.CollectionTypeRegistration;
import org.hibernate.metamodel.CollectionClassification;
/**
* @author Steve Ebersole
*/
@Table(name = "unique_list_owners_reg")
//tag::ex-collections-custom-type-model[]
@Entity
@CollectionTypeRegistration( type = UniqueListType.class, classification = CollectionClassification.LIST )
public class TheEntityWithUniqueListRegistration {
//end::ex-collections-custom-type-model[]
@Id
private Integer id;
@Basic
private String name;
@CollectionTable(name = "unique_list_contents_reg")
//tag::ex-collections-custom-type-model[]
@ElementCollection
private List<String> strings;
// ...
//end::ex-collections-custom-type-model[]
private TheEntityWithUniqueListRegistration() {
// for use by Hibernate
}
public TheEntityWithUniqueListRegistration(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getStrings() {
return strings;
}
public void setStrings(List<String> strings) {
this.strings = strings;
}
//tag::ex-collections-custom-type-model[]
}
//end::ex-collections-custom-type-model[]