Domain Model
The term domain model comes from the realm of data modeling. It is the model that ultimately describes the problem domain you are working in. Sometimes you will also hear the term persistent classes.
Ultimately the application domain model is the central character in an ORM.
They make up the classes you wish to map. Hibernate works best if these classes follow the Plain Old Java Object (POJO) / JavaBean programming model.
However, none of these rules are hard requirements.
Indeed, Hibernate assumes very little about the nature of your persistent objects. You can express a domain model in other ways (using trees of java.util.Map instances, for example).
Historically applications using Hibernate would have used its proprietary XML mapping file format for this purpose. With the coming of JPA, most of this information is now defined in a way that is portable across ORM/JPA providers using annotations (and/or standardized XML format). This chapter will focus on JPA mapping where possible. For Hibernate mapping features not supported by JPA we will prefer Hibernate extension annotations.
Mapping types
Hibernate understands both the Java and JDBC representations of application data.
The ability to read/write this data from/to the database is the function of a Hibernate type.
A type, in this usage, is an implementation of the org.hibernate.type.Type interface.
This Hibernate type also describes various aspects of behavior of the Java type such as how to check for equality, how to clone values, etc.
|
Usage of the word type
The Hibernate type is neither a Java type nor a SQL data type. It provides information about both of these as well as understanding marshalling between. When you encounter the term type in discussions of Hibernate, it may refer to the Java type, the JDBC type, or the Hibernate type, depending on context. |
To help understand the type categorizations, let’s look at a simple table and domain model that we wish to map.
create table Contact (
id integer not null,
first varchar(255),
last varchar(255),
middle varchar(255),
notes varchar(255),
starred boolean not null,
website varchar(255),
primary key (id)
)
@Entity(name = "Contact")
public static class Contact {
@Id
private Integer id;
private Name name;
private String notes;
private URL website;
private boolean starred;
//Getters and setters are omitted for brevity
}
@Embeddable
public class Name {
private String first;
private String middle;
private String last;
// getters and setters omitted
}
In the broadest sense, Hibernate categorizes types into two groups:
Value types
A value type is a piece of data that does not define its own lifecycle. It is, in effect, owned by an entity, which defines its lifecycle.
Looked at another way, all the state of an entity is made up entirely of value types.
These state fields or JavaBean properties are termed persistent attributes.
The persistent attributes of the Contact class are value types.
Value types are further classified into three sub-categories:
- Basic types
-
in mapping the
Contacttable, all attributes except for name would be basic types. Basic types are discussed in detail in Basic Types - Embeddable types
-
the name attribute is an example of an embeddable type, which is discussed in details in Embeddable Types
- Collection types
-
although not featured in the aforementioned example, collection types are also a distinct category among value types. Collection types are further discussed in Collections
Entity types
Entities, by nature of their unique identifier, exist independently of other objects whereas values do not.
Entities are domain model classes which correlate to rows in a database table, using a unique identifier.
Because of the requirement for a unique identifier, entities exist independently and define their own lifecycle.
The Contact class itself would be an example of an entity.
Mapping entities is discussed in detail in Entity.
Naming strategies
Part of the mapping of an object model to the relational database is mapping names from the object model to the corresponding database names. Hibernate looks at this as 2 stage process:
-
The first stage is determining a proper logical name from the domain model mapping. A logical name can be either explicitly specified by the user (using
@Columnor@Tablee.g.) or it can be implicitly determined by Hibernate through an ImplicitNamingStrategy contract. -
Second is the resolving of this logical name to a physical name which is defined by the PhysicalNamingStrategy contract.
|
Historical NamingStrategy contract
Historically Hibernate defined just a single Also, the NamingStrategy contract was often not flexible enough to properly apply a given naming "rule", either because the API lacked the information to decide or because the API was honestly not well defined as it grew. Due to these limitation, |
At the core, the idea behind each naming strategy is to minimize the amount of repetitive information a developer must provide for mapping a domain model.
|
JPA Compatibility
JPA defines inherent rules about implicit logical name determination. If JPA provider portability is a major concern, or if you really just like the JPA-defined implicit naming rules, be sure to stick with ImplicitNamingStrategyJpaCompliantImpl (the default) Also, JPA defines no separation between logical and physical name. Following the JPA specification, the logical name is the physical name. If JPA provider portability is important, applications should prefer not to specify a PhysicalNamingStrategy. |
ImplicitNamingStrategy
When an entity does not explicitly name the database table that it maps to, we need
to implicitly determine that table name. Or when a particular attribute does not explicitly name
the database column that it maps to, we need to implicitly determine that column name. There are
examples of the role of the org.hibernate.boot.model.naming.ImplicitNamingStrategy contract to
determine a logical name when the mapping did not provide an explicit name.
Hibernate defines multiple ImplicitNamingStrategy implementations out-of-the-box. Applications are also free to plug-in custom implementations.
There are multiple ways to specify the ImplicitNamingStrategy to use. First, applications can specify
the implementation using the hibernate.implicit_naming_strategy configuration setting which accepts:
-
pre-defined "short names" for the out-of-the-box implementations
default-
for
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl- an alias forjpa jpa-
for
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl- the JPA 2.0 compliant naming strategy legacy-hbm-
for
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl- compliant with the original Hibernate NamingStrategy legacy-jpa-
for
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl- compliant with the legacy NamingStrategy developed for JPA 1.0, which was unfortunately unclear in many respects regarding implicit naming rules component-path-
for
org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl- mostly followsImplicitNamingStrategyJpaCompliantImplrules, except that it uses the full composite paths, as opposed to just the ending property part
-
reference to a Class that implements the
org.hibernate.boot.model.naming.ImplicitNamingStrategycontract -
FQN of a class that implements the
org.hibernate.boot.model.naming.ImplicitNamingStrategycontract
Secondly, applications and integrations can leverage org.hibernate.boot.MetadataBuilder#applyImplicitNamingStrategy
to specify the ImplicitNamingStrategy to use. See
Bootstrap for additional details on bootstrapping.
PhysicalNamingStrategy
Many organizations define rules around the naming of database objects (tables, columns, foreign-keys, etc). The idea of a PhysicalNamingStrategy is to help implement such naming rules without having to hard-code them into the mapping via explicit names.
While the purpose of an ImplicitNamingStrategy is to determine that an attribute named accountNumber maps to
a logical column name of accountNumber when not explicitly specified, the purpose of a PhysicalNamingStrategy
would be, for example, to say that the physical column name should instead be abbreviated acct_num.
|
It is true that the resolution to |
The default implementation is to simply use the logical name as the physical name. However applications and integrations can define custom implementations of this PhysicalNamingStrategy contract. Here is an example PhysicalNamingStrategy for a fictitious company named Acme Corp whose naming standards are to:
-
prefer underscore-delimited words rather than camel-casing
-
replace certain words with standard abbreviations
/*
* 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.userguide.naming;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.apache.commons.lang3.StringUtils;
/**
* An example PhysicalNamingStrategy that implements database object naming standards
* for our fictitious company Acme Corp.
* <p/>
* In general Acme Corp prefers underscore-delimited words rather than camel casing.
* <p/>
* Additionally standards call for the replacement of certain words with abbreviations.
*
* @author Steve Ebersole
*/
public class AcmeCorpPhysicalNamingStrategy implements PhysicalNamingStrategy {
private static final Map<String,String> ABBREVIATIONS = buildAbbreviationMap();
@Override
public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
// Acme naming standards do not apply to catalog names
return name;
}
@Override
public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
// Acme naming standards do not apply to schema names
return name;
}
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
final List<String> parts = splitAndReplace( name.getText() );
return jdbcEnvironment.getIdentifierHelper().toIdentifier(
join( parts ),
name.isQuoted()
);
}
@Override
public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
final LinkedList<String> parts = splitAndReplace( name.getText() );
// Acme Corp says all sequences should end with _seq
if ( !"seq".equalsIgnoreCase( parts.getLast() ) ) {
parts.add( "seq" );
}
return jdbcEnvironment.getIdentifierHelper().toIdentifier(
join( parts ),
name.isQuoted()
);
}
@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
final List<String> parts = splitAndReplace( name.getText() );
return jdbcEnvironment.getIdentifierHelper().toIdentifier(
join( parts ),
name.isQuoted()
);
}
private static Map<String, String> buildAbbreviationMap() {
TreeMap<String,String> abbreviationMap = new TreeMap<> ( String.CASE_INSENSITIVE_ORDER );
abbreviationMap.put( "account", "acct" );
abbreviationMap.put( "number", "num" );
return abbreviationMap;
}
private LinkedList<String> splitAndReplace(String name) {
LinkedList<String> result = new LinkedList<>();
for ( String part : StringUtils.splitByCharacterTypeCamelCase( name ) ) {
if ( part == null || part.trim().isEmpty() ) {
// skip null and space
continue;
}
part = applyAbbreviationReplacement( part );
result.add( part.toLowerCase( Locale.ROOT ) );
}
return result;
}
private String applyAbbreviationReplacement(String word) {
if ( ABBREVIATIONS.containsKey( word ) ) {
return ABBREVIATIONS.get( word );
}
return word;
}
private String join(List<String> parts) {
boolean firstPass = true;
String separator = "";
StringBuilder joined = new StringBuilder();
for ( String part : parts ) {
joined.append( separator ).append( part );
if ( firstPass ) {
firstPass = false;
separator = "_";
}
}
return joined.toString();
}
}
There are multiple ways to specify the PhysicalNamingStrategy to use. First, applications can specify
the implementation using the hibernate.physical_naming_strategy configuration setting which accepts:
-
reference to a Class that implements the
org.hibernate.boot.model.naming.PhysicalNamingStrategycontract -
FQN of a class that implements the
org.hibernate.boot.model.naming.PhysicalNamingStrategycontract
Secondly, applications and integrations can leverage org.hibernate.boot.MetadataBuilder#applyPhysicalNamingStrategy.
See Bootstrap for additional details on bootstrapping.
Basic Types
Basic value types usually map a single database column, to a single, non-aggregated Java type. Hibernate provides a number of built-in basic types, which follow the natural mappings recommended by the JDBC specifications.
Internally Hibernate uses a registry of basic types when it needs to resolve a specific org.hibernate.type.Type.
Hibernate-provided BasicTypes
| Hibernate type (org.hibernate.type package) | JDBC type | Java type | BasicTypeRegistry key(s) |
|---|---|---|---|
StringType |
VARCHAR |
java.lang.String |
string, java.lang.String |
MaterializedClob |
CLOB |
java.lang.String |
materialized_clob |
TextType |
LONGVARCHAR |
java.lang.String |
text |
CharacterType |
CHAR |
char, java.lang.Character |
char, java.lang.Character |
BooleanType |
BIT |
boolean, java.lang.Boolean |
boolean, java.lang.Boolean |
NumericBooleanType |
INTEGER, 0 is false, 1 is true |
boolean, java.lang.Boolean |
numeric_boolean |
YesNoType |
CHAR, 'N'/'n' is false, 'Y'/'y' is true. The uppercase value is written to the database. |
boolean, java.lang.Boolean |
yes_no |
TrueFalseType |
CHAR, 'F'/'f' is false, 'T'/'t' is true. The uppercase value is written to the database. |
boolean, java.lang.Boolean |
true_false |
ByteType |
TINYINT |
byte, java.lang.Byte |
byte, java.lang.Byte |
ShortType |
SMALLINT |
short, java.lang.Short |
short, java.lang.Short |
IntegerTypes |
INTEGER |
int, java.lang.Integer |
int, java.lang.Integer |
LongType |
BIGINT |
long, java.lang.Long |
long, java.lang.Long |
FloatType |
FLOAT |
float, java.lang.Float |
float, java.lang.Float |
DoubleType |
DOUBLE |
double, java.lang.Double |
double, java.lang.Double |
BigIntegerType |
NUMERIC |
java.math.BigInteger |
big_integer, java.math.BigInteger |
BigDecimalType |
NUMERIC |
java.math.BigDecimal |
big_decimal, java.math.bigDecimal |
TimestampType |
TIMESTAMP |
java.sql.Timestamp |
timestamp, java.sql.Timestamp |
TimeType |
TIME |
java.sql.Time |
time, java.sql.Time |
DateType |
DATE |
java.sql.Date |
date, java.sql.Date |
CalendarType |
TIMESTAMP |
java.util.Calendar |
calendar, java.util.Calendar |
CalendarDateType |
DATE |
java.util.Calendar |
calendar_date |
CalendarTimeType |
TIME |
java.util.Calendar |
calendar_time |
CurrencyType |
VARCHAR |
java.util.Currency |
currency, java.util.Currency |
LocaleType |
VARCHAR |
java.util.Locale |
locale, java.utility.locale |
TimeZoneType |
VARCHAR, using the TimeZone ID |
java.util.TimeZone |
timezone, java.util.TimeZone |
UrlType |
VARCHAR |
java.net.URL |
url, java.net.URL |
ClassType |
VARCHAR (class FQN) |
java.lang.Class |
class, java.lang.Class |
BlobType |
BLOB |
java.sql.Blob |
blob, java.sql.Blob |
ClobType |
CLOB |
java.sql.Clob |
clob, java.sql.Clob |
BinaryType |
VARBINARY |
byte[] |
binary, byte[] |
MaterializedBlobType |
BLOB |
byte[] |
materialized_blob |
ImageType |
LONGVARBINARY |
byte[] |
image |
WrapperBinaryType |
VARBINARY |
java.lang.Byte[] |
wrapper-binary, Byte[], java.lang.Byte[] |
CharArrayType |
VARCHAR |
char[] |
characters, char[] |
CharacterArrayType |
VARCHAR |
java.lang.Character[] |
wrapper-characters, Character[], java.lang.Character[] |
UUIDBinaryType |
BINARY |
java.util.UUID |
uuid-binary, java.util.UUID |
UUIDCharType |
CHAR, can also read VARCHAR |
java.util.UUID |
uuid-char |
PostgresUUIDType |
PostgreSQL UUID, through Types#OTHER, which complies to the PostgreSQL JDBC driver definition |
java.util.UUID |
pg-uuid |
SerializableType |
VARBINARY |
implementors of java.lang.Serializable |
Unlike the other value types, multiple instances of this type are registered. It is registered once under java.io.Serializable, and registered under the specific java.io.Serializable implementation class names. |
StringNVarcharType |
NVARCHAR |
java.lang.String |
nstring |
NTextType |
LONGNVARCHAR |
java.lang.String |
ntext |
NClobType |
NCLOB |
java.sql.NClob |
nclob, java.sql.NClob |
MaterializedNClobType |
NCLOB |
java.lang.String |
materialized_nclob |
PrimitiveCharacterArrayNClobType |
NCHAR |
char[] |
N/A |
CharacterNCharType |
NCHAR |
java.lang.Character |
ncharacter |
CharacterArrayNClobType |
NCLOB |
java.lang.Character[] |
N/A |
| Hibernate type (org.hibernate.type package) | JDBC type | Java type | BasicTypeRegistry key(s) |
|---|---|---|---|
DurationType |
BIGINT |
java.time.Duration |
Duration, java.time.Duration |
InstantType |
TIMESTAMP |
java.time.Instant |
Instant, java.time.Instant |
LocalDateTimeType |
TIMESTAMP |
java.time.LocalDateTime |
LocalDateTime, java.time.LocalDateTime |
LocalDateType |
DATE |
java.time.LocalDate |
LocalDate, java.time.LocalDate |
LocalTimeType |
TIME |
java.time.LocalTime |
LocalTime, java.time.LocalTime |
OffsetDateTimeType |
TIMESTAMP |
java.time.OffsetDateTime |
OffsetDateTime, java.time.OffsetDateTime |
OffsetTimeType |
TIME |
java.time.OffsetTime |
OffsetTime, java.time.OffsetTime |
ZonedDateTimeType |
TIMESTAMP |
java.time.ZonedDateTime |
ZonedDateTime, java.time.ZonedDateTime |
| Hibernate type (org.hibernate.spatial package) | JDBC type | Java type | BasicTypeRegistry key(s) |
|---|---|---|---|
JTSGeometryType |
depends on the dialect |
com.vividsolutions.jts.geom.Geometry |
jts_geometry, or the classname of Geometry or any of its subclasses |
GeolatteGeometryType |
depends on the dialect |
org.geolatte.geom.Geometry |
geolatte_geometry, or the classname of Geometry or any of its subclasses |
|
To use these hibernate-spatial types, you must add the |
These mappings are managed by a service inside Hibernate called the org.hibernate.type.BasicTypeRegistry, which essentially maintains a map of org.hibernate.type.BasicType (a org.hibernate.type.Type specialization) instances keyed by a name.
That is the purpose of the "BasicTypeRegistry key(s)" column in the previous tables.
The @Basic annotation
Strictly speaking, a basic type is denoted by the javax.persistence.Basic annotation.
Generally speaking, the @Basic annotation can be ignored, as it is assumed by default.
Both of the following examples are ultimately the same.
@Basic declared explicitly@Entity(name = "Product")
public class Product {
@Id
@Basic
private Integer id;
@Basic
private String sku;
@Basic
private String name;
@Basic
private String description;
}
@Basic being implicitly implied@Entity(name = "Product")
public class Product {
@Id
private Integer id;
private String sku;
private String name;
private String description;
}
|
The JPA specification strictly limits the Java types that can be marked as basic to the following listing:
If provider portability is a concern, you should stick to just these basic types.
Note that JPA 2.1 did add the notion of a |
The @Basic annotation defines 2 attributes.
optional- boolean (defaults to true)-
Defines whether this attribute allows nulls. JPA defines this as "a hint", which essentially means that it effect is specifically required. As long as the type is not primitive, Hibernate takes this to mean that the underlying column should be
NULLABLE. fetch- FetchType (defaults to EAGER)-
Defines whether this attribute should be fetched eagerly or lazily. JPA says that EAGER is a requirement to the provider (Hibernate) that the value should be fetched when the owner is fetched, while LAZY is merely a hint that the value be fetched when the attribute is accessed. Hibernate ignores this setting for basic types unless you are using bytecode enhancement. See the BytecodeEnhancement for additional information on fetching and on bytecode enhancement.
The @Column annotation
JPA defines rules for implicitly determining the name of tables and columns. For a detailed discussion of implicit naming see Naming.
For basic type attributes, the implicit naming rule is that the column name is the same as the attribute name. If that implicit naming rule does not meet your requirements, you can explicitly tell Hibernate (and other providers) the column name to use.
@Entity(name = "Product")
public class Product {
@Id
private Integer id;
private String sku;
private String name;
@Column( name = "NOTES" )
private String description;
}
Here we use @Column to explicitly map the description attribute to the NOTES column, as opposed to the implicit column name description.
The @Column annotation defines other mapping information as well. See its Javadocs for details.
BasicTypeRegistry
We said before that a Hibernate type is not a Java type, nor a SQL type, but that it understands both and performs the marshalling between them.
But looking at the basic type mappings from the previous examples,
how did Hibernate know to use its org.hibernate.type.StringType for mapping for java.lang.String attributes,
or its org.hibernate.type.IntegerType for mapping java.lang.Integer attributes?
The answer lies in a service inside Hibernate called the org.hibernate.type.BasicTypeRegistry, which essentially maintains a map of org.hibernate.type.BasicType (a org.hibernate.type.Type specialization) instances keyed by a name.
We will see later, in the Explicit BasicTypes section, that we can explicitly tell Hibernate which BasicType to use for a particular attribute. But first let’s explore how implicit resolution works and how applications can adjust implicit resolution.
|
A thorough discussion of the |
As an example, take a String attribute such as we saw before with Product#sku.
Since there was no explicit type mapping, Hibernate looks to the BasicTypeRegistry to find the registered mapping for java.lang.String.
This goes back to the "BasicTypeRegistry key(s)" column we saw in the tables at the start of this chapter.
As a baseline within BasicTypeRegistry, Hibernate follows the recommended mappings of JDBC for Java types.
JDBC recommends mapping Strings to VARCHAR, which is the exact mapping that StringType handles.
So that is the baseline mapping within BasicTypeRegistry for Strings.
Applications can also extend (add new BasicType registrations) or override (replace an existing BasicType registration) using one of the
MetadataBuilder#applyBasicType methods or the MetadataBuilder#applyTypes method during bootstrap.
For more details, see Custom BasicTypes section.
Explicit BasicTypes
Sometimes you want a particular attribute to be handled differently.
Occasionally Hibernate will implicitly pick a BasicType that you do not want (and for some reason you do not want to adjust the BasicTypeRegistry).
In these cases you must explicitly tell Hibernate the BasicType to use, via the org.hibernate.annotations.Type annotation.
@org.hibernate.annotations.Type@Entity(name = "Product")
public class Product {
@Id
private Integer id;
private String sku;
@org.hibernate.annotations.Type( type = "nstring" )
private String name;
@org.hibernate.annotations.Type( type = "materialized_nclob" )
private String description;
}
This tells Hibernate to store the Strings as nationalized data. This is just for illustration purposes; for better ways to indicate nationalized character data see Mapping Nationalized Character Data section.
Additionally, the description is to be handled as a LOB. Again, for better ways to indicate LOBs see Mapping LOBs section.
The org.hibernate.annotations.Type#type attribute can name any of the following:
-
Fully qualified name of any
org.hibernate.type.Typeimplementation -
Any key registered with
BasicTypeRegistry -
The name of any known type definitions
Custom BasicTypes
Hibernate makes it relatively easy for developers to create their own basic type mappings type.
For example, you might want to persist properties of type java.util.BigInteger to VARCHAR columns, or support completely new types.
There are two approaches to developing a custom type:
-
implementing a
BasicTypeand registering it -
implementing a
UserTypewhich doesn’t require type registration
As a means of illustrating the different approaches, let’s consider a use case where we need to support a java.util.BitSet mapping that’s stored as a VARCHAR.
Implementing a BasicType
The first approach is to directly implement the BasicType interface.
|
Because the |
First, we need to extend the AbstractSingleColumnStandardBasicType like this:
BasicType implementationpublic class BitSetType
extends AbstractSingleColumnStandardBasicType<BitSet>
implements DiscriminatorType<BitSet> {
public static final BitSetType INSTANCE = new BitSetType();
public BitSetType() {
super( VarcharTypeDescriptor.INSTANCE, BitSetTypeDescriptor.INSTANCE );
}
@Override
public BitSet stringToObject(String xml) throws Exception {
return fromString( xml );
}
@Override
public String objectToSQLString(BitSet value, Dialect dialect) throws Exception {
return toString( value );
}
@Override
public String getName() {
return "bitset";
}
}
The AbstractSingleColumnStandardBasicType requires an sqlTypeDescriptor and a javaTypeDescriptor.
The sqlTypeDescriptor is VarcharTypeDescriptor.INSTANCE because the database column is a VARCHAR.
On the Java side, we need to use a BitSetTypeDescriptor instance which can be implemented like this:
AbstractTypeDescriptor implementationpublic class BitSetTypeDescriptor extends AbstractTypeDescriptor<BitSet> {
private static final String DELIMITER = ",";
public static final BitSetTypeDescriptor INSTANCE = new BitSetTypeDescriptor();
public BitSetTypeDescriptor() {
super( BitSet.class );
}
@Override
public String toString(BitSet value) {
StringBuilder builder = new StringBuilder();
for ( long token : value.toLongArray() ) {
if ( builder.length() > 0 ) {
builder.append( DELIMITER );
}
builder.append( Long.toString( token, 2 ) );
}
return builder.toString();
}
@Override
public BitSet fromString(String string) {
if ( string == null || string.isEmpty() ) {
return null;
}
String[] tokens = string.split( DELIMITER );
long[] values = new long[tokens.length];
for ( int i = 0; i < tokens.length; i++ ) {
values[i] = Long.valueOf( tokens[i], 2 );
}
return BitSet.valueOf( values );
}
@SuppressWarnings({"unchecked"})
public <X> X unwrap(BitSet value, Class<X> type, WrapperOptions options) {
if ( value == null ) {
return null;
}
if ( BitSet.class.isAssignableFrom( type ) ) {
return (X) value;
}
if ( String.class.isAssignableFrom( type ) ) {
return (X) toString( value);
}
throw unknownUnwrap( type );
}
public <X> BitSet wrap(X value, WrapperOptions options) {
if ( value == null ) {
return null;
}
if ( String.class.isInstance( value ) ) {
return fromString( (String) value );
}
if ( BitSet.class.isInstance( value ) ) {
return (BitSet) value;
}
throw unknownWrap( value.getClass() );
}
}
The unwrap method is used when passing a BitSet as a PreparedStatement bind parameter, while the wrap method is used to transform the JDBC column value object (e.g. String in our case) to the actual mapping object type (e.g. BitSet in this example).
The BasicType must be registered, and this can be done at bootstrapping time:
BasicType implementationconfiguration.registerTypeContributor( (typeContributions, serviceRegistry) -> {
typeContributions.contributeType( BitSetType.INSTANCE );
} );
or using the MetadataBuilder
ServiceRegistry standardRegistry =
new StandardServiceRegistryBuilder().build();
MetadataSources sources = new MetadataSources( standardRegistry );
MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
metadataBuilder.applyBasicType( BitSetType.INSTANCE );
With the new BitSetType being registered as bitset, the entity mapping looks like this:
BasicType mapping@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
@Type( type = "bitset" )
private BitSet bitSet;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public BitSet getBitSet() {
return bitSet;
}
public void setBitSet(BitSet bitSet) {
this.bitSet = bitSet;
}
}
Alternatively, use can use a @TypeDef ans skip the registration phase:
@TypeDef to register a custom Type@Entity(name = "Product")
@TypeDef(
name = "bitset",
defaultForType = BitSet.class,
typeClass = BitSetType.class
)
public static class Product {
@Id
private Integer id;
private BitSet bitSet;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public BitSet getBitSet() {
return bitSet;
}
public void setBitSet(BitSet bitSet) {
this.bitSet = bitSet;
}
}
To validate this new BasicType implementation, we can test it as follows:
BasicTypeBitSet bitSet = BitSet.valueOf( new long[] {1, 2, 3} );
doInHibernate( this::sessionFactory, session -> {
Product product = new Product( );
product.setId( 1 );
product.setBitSet( bitSet );
session.persist( product );
} );
doInHibernate( this::sessionFactory, session -> {
Product product = session.get( Product.class, 1 );
assertEquals(bitSet, product.getBitSet());
} );
When executing this unit test, Hibernate generates the following SQL statements:
BasicTypeDEBUG SQL:92 -
insert
into
Product
(bitSet, id)
values
(?, ?)
TRACE BasicBinder:65 - binding parameter [1] as [VARCHAR] - [{0, 65, 128, 129}]
TRACE BasicBinder:65 - binding parameter [2] as [INTEGER] - [1]
DEBUG SQL:92 -
select
bitsettype0_.id as id1_0_0_,
bitsettype0_.bitSet as bitSet2_0_0_
from
Product bitsettype0_
where
bitsettype0_.id=?
TRACE BasicBinder:65 - binding parameter [1] as [INTEGER] - [1]
TRACE BasicExtractor:61 - extracted value ([bitSet2_0_0_] : [VARCHAR]) - [{0, 65, 128, 129}]
As you can see, the BitSetType takes care of the Java-to-SQL and SQL-to-Java type conversion.
Implementing a UserType
The second approach is to implement the UserType interface.
UserType implementationpublic class BitSetUserType implements UserType {
public static final BitSetUserType INSTANCE = new BitSetUserType();
private static final Logger log = Logger.getLogger( BitSetUserType.class );
@Override
public int[] sqlTypes() {
return new int[] {StringType.INSTANCE.sqlType()};
}
@Override
public Class returnedClass() {
return BitSet.class;
}
@Override
public boolean equals(Object x, Object y)
throws HibernateException {
return Objects.equals( x, y );
}
@Override
public int hashCode(Object x)
throws HibernateException {
return Objects.hashCode( x );
}
@Override
public Object nullSafeGet(
ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
String columnName = names[0];
String columnValue = (String) rs.getObject( columnName );
log.debugv("Result set column {0} value is {1}", columnName, columnValue);
return columnValue == null ? null :
BitSetTypeDescriptor.INSTANCE.fromString( columnValue );
}
@Override
public void nullSafeSet(
PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if ( value == null ) {
log.debugv("Binding null to parameter {0} ",index);
st.setNull( index, Types.VARCHAR );
}
else {
String stringValue = BitSetTypeDescriptor.INSTANCE.toString( (BitSet) value );
log.debugv("Binding {0} to parameter {1} ", stringValue, index);
st.setString( index, stringValue );
}
}
@Override
public Object deepCopy(Object value)
throws HibernateException {
return value == null ? null :
BitSet.valueOf( BitSet.class.cast( value ).toLongArray() );
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(Object value)
throws HibernateException {
return (BitSet) deepCopy( value );
}
@Override
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
return deepCopy( cached );
}
@Override
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
return deepCopy( original );
}
}
The entity mapping looks as follows:
UserType mapping@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
@Type( type = "bitset" )
private BitSet bitSet;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public BitSet getBitSet() {
return bitSet;
}
public void setBitSet(BitSet bitSet) {
this.bitSet = bitSet;
}
}
In this example, the UserType is registered under the bitset name, and this is done like this:
UserType implementationconfiguration.registerTypeContributor( (typeContributions, serviceRegistry) -> {
typeContributions.contributeType( BitSetUserType.INSTANCE, "bitset");
} );
or using the MetadataBuilder
ServiceRegistry standardRegistry =
new StandardServiceRegistryBuilder().build();
MetadataSources sources = new MetadataSources( standardRegistry );
MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
metadataBuilder.applyBasicType( BitSetUserType.INSTANCE, "bitset" );
|
Like Without registration, the
|
When running the previous test case against the BitSetUserType entity mapping, Hibernate executed the following SQL statements:
BasicTypeDEBUG SQL:92 -
insert
into
Product
(bitSet, id)
values
(?, ?)
DEBUG BitSetUserType:71 - Binding 1,10,11 to parameter 1
TRACE BasicBinder:65 - binding parameter [2] as [INTEGER] - [1]
DEBUG SQL:92 -
select
bitsetuser0_.id as id1_0_0_,
bitsetuser0_.bitSet as bitSet2_0_0_
from
Product bitsetuser0_
where
bitsetuser0_.id=?
TRACE BasicBinder:65 - binding parameter [1] as [INTEGER] - [1]
DEBUG BitSetUserType:56 - Result set column bitSet2_0_0_ value is 1,10,11
Mapping enums
Hibernate supports the mapping of Java enums as basic value types in a number of different ways.
@Enumerated
The original JPA-compliant way to map enums was via the @Enumerated and @MapKeyEnumerated for map keys annotations which works on the principle that the enum values are stored according to one of 2 strategies indicated by javax.persistence.EnumType:
ORDINAL-
stored according to the enum value’s ordinal position within the enum class, as indicated by
java.lang.Enum#ordinal STRING-
stored according to the enum value’s name, as indicated by
java.lang.Enum#name
Assuming the following enumeration:
PhoneType enumerationpublic enum PhoneType {
LAND_LINE,
MOBILE;
}
In the ORDINAL example, the phone_type column is defined as a (nullable) INTEGER type and would hold:
NULL-
For null values
0-
For the
LAND_LINEenum 1-
For the
MOBILEenum
@Enumerated(ORDINAL) example@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "phone_number")
private String number;
@Enumerated(EnumType.ORDINAL)
@Column(name = "phone_type")
private PhoneType type;
//Getters and setters are omitted for brevity
}
When persisting this entity, Hibernate generates the following SQL statement:
@Enumerated(ORDINAL) mappingPhone phone = new Phone( );
phone.setId( 1L );
phone.setNumber( "123-456-78990" );
phone.setType( PhoneType.MOBILE );
entityManager.persist( phone );
INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 2, 1)
In the STRING example, the phone_type column is defined as a (nullable) VARCHAR type and would hold:
NULL-
For null values
LAND_LINE-
For the
LAND_LINEenum MOBILE-
For the
MOBILEenum
@Enumerated(STRING) example@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "phone_number")
private String number;
@Enumerated(EnumType.STRING)
@Column(name = "phone_type")
private PhoneType type;
//Getters and setters are omitted for brevity
}
Persisting the same entity like in the @Enumerated(ORDINAL) example, Hibernate generates the following SQL statement:
@Enumerated(STRING) mappingINSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 'MOBILE', 1)
AttributeConverter
Let’s consider the following Gender enum which stores its values using the 'M' and 'F' codes.
public enum Gender {
MALE( 'M' ),
FEMALE( 'F' );
private final char code;
Gender(char code) {
this.code = code;
}
public static Gender fromCode(char code) {
if ( code == 'M' || code == 'm' ) {
return MALE;
}
if ( code == 'F' || code == 'f' ) {
return FEMALE;
}
throw new UnsupportedOperationException(
"The code " + code + " is not supported!"
);
}
public char getCode() {
return code;
}
}
You can map enums in a JPA compliant way using a JPA 2.1 AttributeConverter.
AttributeConverter example@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@Convert( converter = GenderConverter.class )
public Gender gender;
//Getters and setters are omitted for brevity
}
@Converter
public static class GenderConverter
implements AttributeConverter<Gender, Character> {
public Character convertToDatabaseColumn( Gender value ) {
if ( value == null ) {
return null;
}
return value.getCode();
}
public Gender convertToEntityAttribute( Character value ) {
if ( value == null ) {
return null;
}
return Gender.fromCode( value );
}
}
Here, the gender column is defined as a CHAR type and would hold:
NULL-
For null values
'M'-
For the
MALEenum 'F'-
For the
FEMALEenum
For additional details on using AttributeConverters, see JPA 2.1 AttributeConverters section.
|
JPA explicitly disallows the use of an AttributeConverter with an attribute marked as |
Mapping an AttributeConverter using HBM mappings
When using HBM mappings, you can still make use of the JPA AttributeConverter because Hibernate supports
such mapping via the type attribute as demonstrated by the following example.
Let’s consider we have an application-specific Money type:
Money typepublic class Money {
private long cents;
public Money(long cents) {
this.cents = cents;
}
public long getCents() {
return cents;
}
public void setCents(long cents) {
this.cents = cents;
}
}
Now, we want to use the Money type when mapping the Account entity:
Account entity using the Money typepublic class Account {
private Long id;
private String owner;
private Money balance;
//Getters and setters are omitted for brevity
}
Since Hibernate has no knowledge how to persist the Money type, we could use a JPA AttributeConverter
to transform the Money type as a Long. For this purpose, we are going to use the following
MoneyConverter utility:
MoneyConverter implementing the JPA AttributeConverter interfacepublic class MoneyConverter
implements AttributeConverter<Money, Long> {
@Override
public Long convertToDatabaseColumn(Money attribute) {
return attribute == null ? null : attribute.getCents();
}
@Override
public Money convertToEntityAttribute(Long dbData) {
return dbData == null ? null : new Money( dbData );
}
}
To map the MoneyConverter using HBM configuration files you need to use the converted:: prefix in the type
attribute of the property element.
AttributeConverter<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.hibernate.userguide.mapping.converter.hbm">
<class name="Account" table="account" >
<id name="id"/>
<property name="owner"/>
<property name="balance"
type="converted::org.hibernate.userguide.mapping.converter.hbm.MoneyConverter"/>
</class>
</hibernate-mapping>
Custom type
You can also map enums using a Hibernate custom type mapping.
Let’s again revisit the Gender enum example, this time using a custom Type to store the more standardized 'M' and 'F' codes.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@Type( type = "org.hibernate.userguide.mapping.basic.GenderType" )
public Gender gender;
//Getters and setters are omitted for brevity
}
public class GenderType extends AbstractSingleColumnStandardBasicType<Gender> {
public static final GenderType INSTANCE = new GenderType();
public GenderType() {
super(
CharTypeDescriptor.INSTANCE,
GenderJavaTypeDescriptor.INSTANCE
);
}
public String getName() {
return "gender";
}
@Override
protected boolean registerUnderJavaType() {
return true;
}
}
public class GenderJavaTypeDescriptor extends AbstractTypeDescriptor<Gender> {
public static final GenderJavaTypeDescriptor INSTANCE =
new GenderJavaTypeDescriptor();
protected GenderJavaTypeDescriptor() {
super( Gender.class );
}
public String toString(Gender value) {
return value == null ? null : value.name();
}
public Gender fromString(String string) {
return string == null ? null : Gender.valueOf( string );
}
public <X> X unwrap(Gender value, Class<X> type, WrapperOptions options) {
return CharacterTypeDescriptor.INSTANCE.unwrap(
value == null ? null : value.getCode(),
type,
options
);
}
public <X> Gender wrap(X value, WrapperOptions options) {
return Gender.fromCode(
CharacterTypeDescriptor.INSTANCE.wrap( value, options )
);
}
}
Again, the gender column is defined as a CHAR type and would hold:
NULL-
For null values
'M'-
For the
MALEenum 'F'-
For the
FEMALEenum
For additional details on using custom types, see Custom BasicTypes section.
Mapping LOBs
Mapping LOBs (database Large Objects) come in 2 forms, those using the JDBC locator types and those materializing the LOB data.
JDBC LOB locators exist to allow efficient access to the LOB data. They allow the JDBC driver to stream parts of the LOB data as needed, potentially freeing up memory space. However they can be unnatural to deal with and have certain limitations. For example, a LOB locator is only portably valid during the duration of the transaction in which it was obtained.
The idea of materialized LOBs is to trade-off the potential efficiency (not all drivers handle LOB data efficiently) for a more natural programming paradigm using familiar Java types such as String or byte[], etc for these LOBs.
Materialized deals with the entire LOB contents in memory, whereas LOB locators (in theory) allow streaming parts of the LOB contents into memory as needed.
The JDBC LOB locator types include:
-
java.sql.Blob -
java.sql.Clob -
java.sql.NClob
Mapping materialized forms of these LOB values would use more familiar Java types such as String, char[], byte[], etc.
The trade off for more familiar is usually performance.
Mapping CLOB
For a first look, let’s assume we have a CLOB column that we would like to map (NCLOB character LOB data will be covered in Mapping Nationalized Character Data section).
Considering we have the following database table:
CREATE TABLE Product (
id INTEGER NOT NULL,
name VARCHAR(255),
warranty CLOB,
PRIMARY KEY (id)
)
Let’s first map this using the @Lob JPA annotation and the java.sql.Clob type:
CLOB mapped to java.sql.Clob@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
private Clob warranty;
//Getters and setters are omitted for brevity
}
To persist such an entity, you have to create a Clob using the ClobProxy Hibernate utility:
java.sql.Clob entityString warranty = "My product warranty";
final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );
product.setWarranty( ClobProxy.generateProxy( warranty ) );
entityManager.persist( product );
To retrieve the Clob content, you need to transform the underlying java.io.Reader:
java.sql.Clob entityProduct product = entityManager.find( Product.class, productId );
try (Reader reader = product.getWarranty().getCharacterStream()) {
assertEquals( "My product warranty", toString( reader ) );
}
We could also map the CLOB in a materialized form. This way, we can either use a String or a char[].
CLOB mapped to String@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
private String warranty;
//Getters and setters are omitted for brevity
}
|
How JDBC deals with However, some drivers are trickier (e.g. PostgreSQL), and, in such cases, you may have to do some extra to get LOBs working. Such discussions are beyond the scope of this guide. |
We might even want the materialized data as a char array (although this might not be a very good idea).
char[] mapping@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
private char[] warranty;
//Getters and setters are omitted for brevity
}
Mapping BLOB
BLOB data is mapped in a similar fashion.
Considering we have the following database table:
CREATE TABLE Product (
id INTEGER NOT NULL ,
image blob ,
name VARCHAR(255) ,
PRIMARY KEY ( id )
)
Let’s first map this using the JDBC java.sql.Blob type.
BLOB mapped to java.sql.Blob@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
private Blob image;
//Getters and setters are omitted for brevity
}
To persist such an entity, you have to create a Blob using the BlobProxy Hibernate utility:
java.sql.Blob entitybyte[] image = new byte[] {1, 2, 3};
final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );
product.setImage( BlobProxy.generateProxy( image ) );
entityManager.persist( product );
To retrieve the Blob content, you need to transform the underlying java.io.InputStream:
java.sql.Blob entityProduct product = entityManager.find( Product.class, productId );
try (InputStream inputStream = product.getImage().getBinaryStream()) {
assertArrayEquals(new byte[] {1, 2, 3}, toBytes( inputStream ) );
}
We could also map the BLOB in a materialized form (e.g. byte[]).
BLOB mapped to byte[]@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
private byte[] image;
//Getters and setters are omitted for brevity
}
Mapping Nationalized Character Data
JDBC 4 added the ability to explicitly handle nationalized character data. To this end it added specific nationalized character data types.
-
NCHAR -
NVARCHAR -
LONGNVARCHAR -
NCLOB
Considering we have the following database table:
NVARCHAR - SQLCREATE TABLE Product (
id INTEGER NOT NULL ,
name VARCHAR(255) ,
warranty NVARCHAR(255) ,
PRIMARY KEY ( id )
)
To map a specific attribute to a nationalized variant data type, Hibernate defines the @Nationalized annotation.
NVARCHAR mapping@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Nationalized
private String warranty;
//Getters and setters are omitted for brevity
}
Just like with CLOB, Hibernate can also deal with NCLOB SQL data types:
NCLOB - SQLCREATE TABLE Product (
id INTEGER NOT NULL ,
name VARCHAR(255) ,
warranty nclob ,
PRIMARY KEY ( id )
)
Hibernate can map the NCLOB to a java.sql.NClob
NCLOB mapped to java.sql.NClob@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
@Nationalized
// Clob also works, because NClob extends Clob.
// The database type is still NCLOB either way and handled as such.
private NClob warranty;
//Getters and setters are omitted for brevity
}
To persist such an entity, you have to create a NClob using the NClobProxy Hibernate utility:
java.sql.NClob entityString warranty = "My product warranty";
final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );
product.setWarranty( NClobProxy.generateProxy( warranty ) );
entityManager.persist( product );
To retrieve the NClob content, you need to transform the underlying java.io.Reader:
java.sql.NClob entityProduct product = entityManager.find( Product.class, productId );
try (Reader reader = product.getWarranty().getCharacterStream()) {
assertEquals( "My product warranty", toString( reader ) );
}
We could also map the NCLOB in a materialized form. This way, we can either use a String or a char[].
NCLOB mapped to String@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
@Nationalized
private String warranty;
//Getters and setters are omitted for brevity
}
We might even want the materialized data as a char array.
char[] mapping@Entity(name = "Product")
public static class Product {
@Id
private Integer id;
private String name;
@Lob
@Nationalized
private char[] warranty;
//Getters and setters are omitted for brevity
}
|
If you application and database are entirely nationalized you may instead want to enable nationalized character data as the default.
You can do this via the |
Mapping UUID Values
Hibernate also allows you to map UUID values, again in a number of ways.
|
The default UUID mapping is as binary because it represents more efficient storage.
However many applications prefer the readability of character storage.
To switch the default mapping, simply call |
UUID as binary
As mentioned, the default mapping for UUID attributes.
Maps the UUID to a byte[] using java.util.UUID#getMostSignificantBits and java.util.UUID#getLeastSignificantBits and stores that as BINARY data.
Chosen as the default simply because it is generally more efficient from storage perspective.
UUID as (var)char
Maps the UUID to a String using java.util.UUID#toString and java.util.UUID#fromString and stores that as CHAR or VARCHAR data.
PostgreSQL-specific UUID
|
When using one of the PostgreSQL Dialects, this becomes the default UUID mapping. |
Maps the UUID using PostgreSQL’s specific UUID data type.
The PostgreSQL JDBC driver chooses to map its UUID type to the OTHER code.
Note that this can cause difficulty as the driver chooses to map many different data types to OTHER.
UUID as identifier
Hibernate supports using UUID values as identifiers, and they can even be generated on user’s behalf. For details, see the discussion of generators in Identifier generators.
Mapping Date/Time Values
Hibernate allows various Java Date/Time classes to be mapped as persistent domain model entity properties. The SQL standard defines three Date/Time types:
- DATE
-
Represents a calendar date by storing years, months and days. The JDBC equivalent is
java.sql.Date - TIME
-
Represents the time of a day and it stores hours, minutes and seconds. The JDBC equivalent is
java.sql.Time - TIMESTAMP
-
It stores both a DATE and a TIME plus nanoseconds. The JDBC equivalent is
java.sql.Timestamp
|
To avoid dependencies on the |
While the java.sql classes define a direct association to the SQL Date/Time data types,
the java.util or java.time properties need to explicitly mark the SQL type correlation with the @Temporal annotation.
This way, a java.util.Date or a java.util.Calendar can be mapped to either an SQL DATE, TIME or TIMESTAMP type.
Considering the following entity:
java.util.Date mapped as DATE@Entity(name = "DateEvent")
public static class DateEvent {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp`")
@Temporal(TemporalType.DATE)
private Date timestamp;
//Getters and setters are omitted for brevity
}
When persisting such entity:
java.util.Date mappingDateEvent dateEvent = new DateEvent( new Date() );
entityManager.persist( dateEvent );
Hibernate generates the following INSERT statement:
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29', 1 )
Only the year, month and the day field were saved into the database.
If we change the @Temporal type to TIME:
java.util.Date mapped as TIME@Column(name = "`timestamp`")
@Temporal(TemporalType.TIME)
private Date timestamp;
Hibernate will issue an INSERT statement containing the hour, minutes and seconds.
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '16:51:58', 1 )
When the @Temporal type is set to TIMESTAMP:
java.util.Date mapped as TIMESTAMP@Column(name = "`timestamp`")
@Temporal(TemporalType.TIMESTAMP)
private Date timestamp;
Hibernate will include both the DATE, the TIME and the nanoseconds in the INSERT statement:
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29 16:54:04.544', 1 )
|
Just like the |
Mapping Java 8 Date/Time Values
Java 8 came with a new Date/Time API, offering support for instant dates, intervals, local and zoned Date/Time immutable instances, bundled in the java.time package.
The mapping between the standard SQL Date/Time types and the supported Java 8 Date/Time class types looks as follows;
- DATE
-
java.time.LocalDate - TIME
-
java.time.LocalTime,java.time.OffsetTime - TIMESTAMP
-
java.time.Instant,java.time.LocalDateTime,java.time.OffsetDateTimeandjava.time.ZonedDateTime
|
Because the mapping between Java 8 Date/Time classes and the SQL types is implicit, there is not need to specify the org.hibernate.AnnotationException: @Temporal should only be set on a java.util.Date or java.util.Calendar property |
Using a specific time zone
By default, Hibernate is going to use the PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp) or
PreparedStatement.setTime(int parameterIndex, java.sql.Time x) when saving a java.sql.Timestamp or a java.sql.Time property.
When the time zone is not specified, the JDBC driver is going to use the underlying JVM default time zone, which might not be suitable if the application is used from all across the globe. For this reason, it is very common to use a single reference time zone (e.g. UTC) whenever saving/loading data from the database.
One alternative would be to configure all JVMs to use the reference time zone:
- Declaratively
-
java -Duser.timezone=UTC ... - Programmatically
-
TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );
However, as explained in this article, this is not always practical especially for front-end nodes.
For this reason, Hibernate offers the hibernate.jdbc.time_zone configuration property which can be configured:
- Declaratively, at the
SessionFactorylevel -
settings.put( AvailableSettings.JDBC_TIME_ZONE, TimeZone.getTimeZone( "UTC" ) ); - Programmatically, on a per
Sessionbasis -
Session session = sessionFactory() .withOptions() .jdbcTimeZone( TimeZone.getTimeZone( "UTC" ) ) .openSession();
With this configuration property in place, Hibernate is going to call the PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp, Calendar cal) or
PreparedStatement.setTime(int parameterIndex, java.sql.Time x, Calendar cal), where the java.util.Calendar references the time zone provided via the hibernate.jdbc.time_zone property.
JPA 2.1 AttributeConverters
Although Hibernate has long been offering custom types, as a JPA 2.1 provider,
it also supports AttributeConverter as well.
With a custom AttributeConverter, the application developer can map a given JDBC type to an entity basic type.
In the following example, the java.time.Period is going to be mapped to a VARCHAR database column.
java.time.Period custom AttributeConverter@Converter
public class PeriodStringConverter
implements AttributeConverter<Period, String> {
@Override
public String convertToDatabaseColumn(Period attribute) {
return attribute.toString();
}
@Override
public Period convertToEntityAttribute(String dbData) {
return Period.parse( dbData );
}
}
To make use of this custom converter, the @Convert annotation must decorate the entity attribute.
java.time.Period AttributeConverter mapping@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Convert(converter = PeriodStringConverter.class)
@Column(columnDefinition = "")
private Period span;
//Getters and setters are omitted for brevity
}
When persisting such entity, Hibernate will do the type conversion based on the AttributeConverter logic:
AttributeConverterINSERT INTO Event ( span, id )
VALUES ( 'P1Y2M3D', 1 )
AttributeConverter Java and JDBC types
In cases when the Java type specified for the "database side" of the conversion (the second AttributeConverter bind parameter) is not known,
Hibernate will fallback to a java.io.Serializable type.
If the Java type is not know to Hibernate, you will encounter the following message:
HHH000481: Encountered Java type for which we could not locate a JavaTypeDescriptor and which does not appear to implement equals and/or hashCode. This can lead to significant performance problems when performing equality/dirty checking involving this Java type. Consider registering a custom JavaTypeDescriptor or at least implementing equals/hashCode.
Whether a Java type is "known" means it has an entry in the JavaTypeDescriptorRegistry.
While by default Hibernate loads many JDK types into the JavaTypeDescriptorRegistry, an application can also expand the JavaTypeDescriptorRegistry by
adding new JavaTypeDescriptor entries.
This way, Hibernate will also know how to handle a specific Java Object type at the JDBC level.
JPA 2.1 AttributeConverter Mutability Plan
A basic type that’s converted by a JPA AttributeConverter is immutable if the underlying Java type is immutable
and is mutable if the associated attribute type is mutable as well.
Therefore, mutability is given by the JavaTypeDescriptor#getMutabilityPlan
of the associated entity attribute type.
Immutable types
If the entity attribute is a String, a primitive wrapper (e.g. Integer, Long) an Enum type, or any other immutable Object type,
then you can only change the entity attribute value by reassigning it to a new value.
Considering we have the same Period entity attribute as illustrated in the JPA 2.1 AttributeConverters section:
@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Convert(converter = PeriodStringConverter.class)
@Column(columnDefinition = "")
private Period span;
//Getters and setters are omitted for brevity
}
The only way to change the span attribute is to reassign it to a different value:
Event event = entityManager.createQuery( "from Event", Event.class ).getSingleResult();
event.setSpan(Period
.ofYears( 3 )
.plusMonths( 2 )
.plusDays( 1 )
);
Mutable types
On the other hand, consider the following example where the Money type is a mutable.
public static class Money {
private long cents;
public Money(long cents) {
this.cents = cents;
}
public long getCents() {
return cents;
}
public void setCents(long cents) {
this.cents = cents;
}
}
public static class MoneyConverter
implements AttributeConverter<Money, Long> {
@Override
public Long convertToDatabaseColumn(Money attribute) {
return attribute == null ? null : attribute.getCents();
}
@Override
public Money convertToEntityAttribute(Long dbData) {
return dbData == null ? null : new Money( dbData );
}
}
@Entity(name = "Account")
public static class Account {
@Id
private Long id;
private String owner;
@Convert(converter = MoneyConverter.class)
private Money balance;
//Getters and setters are omitted for brevity
}
A mutable Object allows you to modify its internal structure, and Hibernate dirty checking mechanism is going to propagate the change to the database:
Account account = entityManager.find( Account.class, 1L );
account.getBalance().setCents( 150 * 100L );
entityManager.persist( account );
|
Although the For this reason, prefer immutable types over mutable ones whenever possible. |
SQL quoted identifiers
You can force Hibernate to quote an identifier in the generated SQL by enclosing the table or column name in backticks in the mapping document. While traditionally, Hibernate used backticks for escaping SQL reserved keywords, JPA uses double quotes instead.
Once the reserved keywords are escaped, Hibernate will use the correct quotation style for the SQL Dialect.
This is usually double quotes, but SQL Server uses brackets and MySQL uses backticks.
@Entity(name = "Product")
public static class Product {
@Id
private Long id;
@Column(name = "`name`")
private String name;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
@Entity(name = "Product")
public static class Product {
@Id
private Long id;
@Column(name = "\"name\"")
private String name;
@Column(name = "\"number\"")
private String number;
//Getters and setters are omitted for brevity
}
Because name and number are reserved words, the Product entity mapping uses backtricks to quote these column names.
When saving the following Product entity, Hibernate generates the following SQL insert statement:
Product product = new Product();
product.setId( 1L );
product.setName( "Mobile phone" );
product.setNumber( "123-456-7890" );
entityManager.persist( product );
INSERT INTO Product ("name", "number", id)
VALUES ('Mobile phone', '123-456-7890', 1)
Global quoting
Hibernate can also quote all identifiers (e.g. table, columns) using the following configuration property:
<property
name="hibernate.globally_quoted_identifiers"
value="true"
/>
This way, we don’t need to manually quote any identifier:
@Entity(name = "Product")
public static class Product {
@Id
private Long id;
private String name;
private String number;
//Getters and setters are omitted for brevity
}
When persisting a Product entity, Hibernate is going to quote all identifiers as in the following example:
INSERT INTO "Product" ("name", "number", "id")
VALUES ('Mobile phone', '123-456-7890', 1)
As you can see, both the table name and all the column have been quoted.
For more about quoting-related configuration properties, check out the Mapping configurations section as well.
Generated properties
Generated properties are properties that have their values generated by the database.
Typically, Hibernate applications needed to refresh objects that contain any properties for which the database was generating values.
Marking properties as generated, however, lets the application delegate this responsibility to Hibernate.
When Hibernate issues an SQL INSERT or UPDATE for an entity that has defined generated properties, it immediately issues a select to retrieve the generated values.
Properties marked as generated must additionally be non-insertable and non-updateable.
Only @Version and @Basic types can be marked as generated.
NEVER(the default)-
the given property value is not generated within the database.
INSERT-
the given property value is generated on insert, but is not regenerated on subsequent updates. Properties like creationTimestamp fall into this category.
ALWAYS-
the property value is generated both on insert and on update.
To mark a property as generated, use The Hibernate specific @Generated annotation.
@Generated annotation
The @Generated annotation is used so that Hibernate can fetch the currently annotated property after the entity has been persisted or updated.
For this reason, the @Generated annotation accepts a GenerationTime enum value.
Considering the following entity:
@Generated mapping example@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String firstName;
private String lastName;
private String middleName1;
private String middleName2;
private String middleName3;
private String middleName4;
private String middleName5;
@Generated( value = GenerationTime.ALWAYS )
@Column(columnDefinition =
"AS CONCAT(" +
" COALESCE(firstName, ''), " +
" COALESCE(' ' + middleName1, ''), " +
" COALESCE(' ' + middleName2, ''), " +
" COALESCE(' ' + middleName3, ''), " +
" COALESCE(' ' + middleName4, ''), " +
" COALESCE(' ' + middleName5, ''), " +
" COALESCE(' ' + lastName, '') " +
")")
private String fullName;
}
When the Person entity is persisted, Hibernate is going to fetch the calculated fullName column from the database,
which concatenates the first, middle, and last name.
@Generated persist examplePerson person = new Person();
person.setId( 1L );
person.setFirstName( "John" );
person.setMiddleName1( "Flávio" );
person.setMiddleName2( "André" );
person.setMiddleName3( "Frederico" );
person.setMiddleName4( "Rúben" );
person.setMiddleName5( "Artur" );
person.setLastName( "Doe" );
entityManager.persist( person );
entityManager.flush();
assertEquals("John Flávio André Frederico Rúben Artur Doe", person.getFullName());
INSERT INTO Person
(
firstName,
lastName,
middleName1,
middleName2,
middleName3,
middleName4,
middleName5,
id
)
values
(?, ?, ?, ?, ?, ?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT] - [1]
SELECT
p.fullName as fullName3_0_
FROM
Person p
WHERE
p.id=?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe]
The same goes when the Person entity is updated.
Hibernate is going to fetch the calculated fullName column from the database after the entity is modified.
@Generated update examplePerson person = entityManager.find( Person.class, 1L );
person.setLastName( "Doe Jr" );
entityManager.flush();
assertEquals("John Flávio André Frederico Rúben Artur Doe Jr", person.getFullName());
UPDATE
Person
SET
firstName=?,
lastName=?,
middleName1=?,
middleName2=?,
middleName3=?,
middleName4=?,
middleName5=?
WHERE
id=?
-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe Jr]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT] - [1]
SELECT
p.fullName as fullName3_0_
FROM
Person p
WHERE
p.id=?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe Jr]
@GeneratorType annotation
The @GeneratorType annotation is used so that
you can provide a custom generator to set the value of the currently annotated property.
For this reason, the @GeneratorType annotation accepts a GenerationTime enum value
and a custom ValueGenerator class type.
Considering the following entity:
@GeneratorType mapping examplepublic static class CurrentUser {
public static final CurrentUser INSTANCE = new CurrentUser();
private static final ThreadLocal<String> storage = new ThreadLocal<>();
public void logIn(String user) {
storage.set( user );
}
public void logOut() {
storage.remove();
}
public String get() {
return storage.get();
}
}
public static class LoggedUserGenerator implements ValueGenerator<String> {
@Override
public String generateValue(
Session session, Object owner) {
return CurrentUser.INSTANCE.get();
}
}
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String firstName;
private String lastName;
@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.INSERT)
private String createdBy;
@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.ALWAYS)
private String updatedBy;
}
When the Person entity is persisted, Hibernate is going to populate the createdBy column with the currently logged user.
@Generated persist exampleCurrentUser.INSTANCE.logIn( "Alice" );
doInJPA( this::entityManagerFactory, entityManager -> {
Person person = new Person();
person.setId( 1L );
person.setFirstName( "John" );
person.setLastName( "Doe" );
entityManager.persist( person );
} );
CurrentUser.INSTANCE.logOut();
INSERT INTO Person
(
createdBy,
firstName,
lastName,
updatedBy,
id
)
VALUES
(?, ?, ?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Alice]
-- binding parameter [5] as [BIGINT] - [1]
The same goes when the Person entity is updated.
Hibernate is going to populate the updatedBy column with the currently logged user.
@Generated update exampleCurrentUser.INSTANCE.logIn( "Bob" );
doInJPA( this::entityManagerFactory, entityManager -> {
Person person = entityManager.find( Person.class, 1L );
person.setFirstName( "Mr. John" );
} );
CurrentUser.INSTANCE.logOut();
UPDATE Person
SET
createdBy = ?,
firstName = ?,
lastName = ?,
updatedBy = ?
WHERE
id = ?
-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [Mr. John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Bob]
-- binding parameter [5] as [BIGINT] - [1]
@CreationTimestamp annotation
The @CreationTimestamp annotation instructs Hibernate to set the annotated entity attribute with the current timestamp value of the JVM
when the entity is being persisted.
The supported property types are:
-
java.util.Date -
java.util.Calendar -
java.sql.Date -
java.sql.Time -
java.sql.Timestamp
@CreationTimestamp mapping example@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp`")
@CreationTimestamp
private Date timestamp;
public Event() {}
public Long getId() {
return id;
}
public Date getTimestamp() {
return timestamp;
}
}
When the Event entity is persisted, Hibernate is going to populate the underlying timestamp column with the current JVM timestamp value:
@CreationTimestamp persist exampleEvent dateEvent = new Event( );
entityManager.persist( dateEvent );
INSERT INTO Event ("timestamp", id)
VALUES (?, ?)
-- binding parameter [1] as [TIMESTAMP] - [Tue Nov 15 16:24:20 EET 2016]
-- binding parameter [2] as [BIGINT] - [1]
@UpdateTimestamp annotation
The @UpdateTimestamp annotation instructs Hibernate to set the annotated entity attribute with the current timestamp value of the JVM
when the entity is being persisted.
The supported property types are:
-
java.util.Date -
java.util.Calendar -
java.sql.Date -
java.sql.Time -
java.sql.Timestamp
@UpdateTimestamp mapping example@Entity(name = "Bid")
public static class Bid {
@Id
@GeneratedValue
private Long id;
@Column(name = "updated_on")
@UpdateTimestamp
private Date updatedOn;
@Column(name = "updated_by")
private String updatedBy;
private Long cents;
//Getters and setters are omitted for brevity
}
When the Bid entity is persisted, Hibernate is going to populate the underlying updated_on column with the current JVM timestamp value:
@UpdateTimestamp persist exampleBid bid = new Bid();
bid.setUpdatedBy( "John Doe" );
bid.setCents( 150 * 100L );
entityManager.persist( bid );
INSERT INTO Bid (cents, updated_by, updated_on, id)
VALUES (?, ?, ?, ?)
-- binding parameter [1] as [BIGINT] - [15000]
-- binding parameter [2] as [VARCHAR] - [John Doe]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:21:46 EEST 2017]
-- binding parameter [4] as [BIGINT] - [1]
When updating the Bid entity, Hibernate is going to modify the updated_on column with the current JVM timestamp value:
@UpdateTimestamp update exampleBid bid = entityManager.find( Bid.class, 1L );
bid.setUpdatedBy( "John Doe Jr." );
bid.setCents( 160 * 100L );
entityManager.persist( bid );
UPDATE Bid SET
cents = ?,
updated_by = ?,
updated_on = ?
where
id = ?
-- binding parameter [1] as [BIGINT] - [16000]
-- binding parameter [2] as [VARCHAR] - [John Doe Jr.]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:49:24 EEST 2017]
-- binding parameter [4] as [BIGINT] - [1]
@ValueGenerationType meta-annotation
Hibernate 4.3 introduced the @ValueGenerationType meta-annotation, which is a new approach to declaring generated attributes or customizing generators.
@Generated has been retrofitted to use the @ValueGenerationType meta-annotation.
But @ValueGenerationType exposes more features than what @Generated currently supports, and,
to leverage some of those features, you’d simply wire up a new generator annotation.
As you’ll see in the following examples, the @ValueGenerationType meta-annotation is used when declaring the custom annotation used to mark the entity properties that need a specific generation strategy.
The actual generation logic must be added to the class that implements the AnnotationValueGeneration interface.
Database-generated values
For example, let’s say we want the timestamps to be generated by calls to the standard ANSI SQL function current_timestamp (rather than triggers or DEFAULT values):
ValueGenerationType mapping for database generation@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp`")
@FunctionCreationTimestamp
private Date timestamp;
public Event() {}
public Long getId() {
return id;
}
public Date getTimestamp() {
return timestamp;
}
}
@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}
public static class FunctionCreationValueGeneration
implements AnnotationValueGeneration<FunctionCreationTimestamp> {
@Override
public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
}
/**
* Generate value on INSERT
* @return when to generate the value
*/
public GenerationTiming getGenerationTiming() {
return GenerationTiming.INSERT;
}
/**
* Returns null because the value is generated by the database.
* @return null
*/
public ValueGenerator<?> getValueGenerator() {
return null;
}
/**
* Returns true because the value is generated by the database.
* @return true
*/
public boolean referenceColumnInSql() {
return true;
}
/**
* Returns the database-generated value
* @return database-generated value
*/
public String getDatabaseGeneratedReferencedColumnValue() {
return "current_timestamp";
}
}
When persisting an Event entity, Hibernate generates the following SQL statement:
INSERT INTO Event ("timestamp", id)
VALUES (current_timestamp, 1)
As you can see, the current_timestamp value was used for assigning the timestamp column value.
In-memory-generated values
If the timestamp value needs to be generated in-memory, the following mapping must be used instead:
ValueGenerationType mapping for in-memory value generation@Entity(name = "Event")
public static class Event {
@Id
@GeneratedValue
private Long id;
@Column(name = "`timestamp`")
@FunctionCreationTimestamp
private Date timestamp;
public Event() {}
public Long getId() {
return id;
}
public Date getTimestamp() {
return timestamp;
}
}
@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}
public static class FunctionCreationValueGeneration
implements AnnotationValueGeneration<FunctionCreationTimestamp> {
@Override
public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
}
/**
* Generate value on INSERT
* @return when to generate the value
*/
public GenerationTiming getGenerationTiming() {
return GenerationTiming.INSERT;
}
/**
* Returns the in-memory generated value
* @return {@code true}
*/
public ValueGenerator<?> getValueGenerator() {
return (session, owner) -> new Date( );
}
/**
* Returns false because the value is generated by the database.
* @return false
*/
public boolean referenceColumnInSql() {
return false;
}
/**
* Returns null because the value is generated in-memory.
* @return null
*/
public String getDatabaseGeneratedReferencedColumnValue() {
return null;
}
}
When persisting an Event entity, Hibernate generates the following SQL statement:
INSERT INTO Event ("timestamp", id)
VALUES ('Tue Mar 01 10:58:18 EET 2016', 1)
As you can see, the new Date() object value was used for assigning the timestamp column value.
Column transformers: read and write expressions
Hibernate allows you to customize the SQL it uses to read and write the values of columns mapped to @Basic types.
For example, if your database provides a set of data encryption functions, you can invoke them for individual columns like in the following example.
@ColumnTransformer example@Entity(name = "Employee")
public static class Employee {
@Id
private Long id;
@NaturalId
private String username;
@Column(name = "pswd")
@ColumnTransformer(
read = "decrypt( 'AES', '00', pswd )",
write = "encrypt('AES', '00', ?)"
)
private String password;
private int accessLevel;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
@ManyToMany(mappedBy = "employees")
private List<Project> projects = new ArrayList<>();
//Getters and setters omitted for brevity
}
|
You can use the plural form |
If a property uses more than one column, you must use the forColumn attribute to specify which column, the expressions are targeting.
@ColumnTransformer forColumn attribute usage@Entity(name = "Savings")
public static class Savings {
@Id
private Long id;
@Type(type = "org.hibernate.userguide.mapping.basic.MonetaryAmountUserType")
@Columns(columns = {
@Column(name = "money"),
@Column(name = "currency")
})
@ColumnTransformer(
forColumn = "money",
read = "money / 100",
write = "? * 100"
)
private MonetaryAmount wallet;
//Getters and setters omitted for brevity
}
Hibernate applies the custom expressions automatically whenever the property is referenced in a query.
This functionality is similar to a derived-property @Formula with two differences:
-
The property is backed by one or more columns that are exported as part of automatic schema generation.
-
The property is read-write, not read-only.
The write expression, if specified, must contain exactly one '?' placeholder for the value.
@ColumnTransformer and a composite typedoInJPA( this::entityManagerFactory, entityManager -> {
Savings savings = new Savings( );
savings.setId( 1L );
savings.setWallet( new MonetaryAmount( BigDecimal.TEN, Currency.getInstance( Locale.US ) ) );
entityManager.persist( savings );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Savings savings = entityManager.find( Savings.class, 1L );
assertEquals( 10, savings.getWallet().getAmount().intValue());
} );
INSERT INTO Savings (money, currency, id)
VALUES (10 * 100, 'USD', 1)
SELECT
s.id as id1_0_0_,
s.money / 100 as money2_0_0_,
s.currency as currency3_0_0_
FROM
Savings s
WHERE
s.id = 1
@Formula
Sometimes, you want the Database to do some computation for you rather than in the JVM, you might also create some kind of virtual column. You can use a SQL fragment (aka formula) instead of mapping a property into a column. This kind of property is read only (its value is calculated by your formula fragment)
|
You should be aware that the |
@Formula mapping usage@Entity(name = "Account")
public static class Account {
@Id
private Long id;
private Double credit;
private Double rate;
@Formula(value = "credit * rate")
private Double interest;
//Getters and setters omitted for brevity
}
When loading the Account entity, Hibernate is going to calculate the interest property using the configured @Formula:
@Formula mappingdoInJPA( this::entityManagerFactory, entityManager -> {
Account account = new Account( );
account.setId( 1L );
account.setCredit( 5000d );
account.setRate( 1.25 / 100 );
entityManager.persist( account );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Account account = entityManager.find( Account.class, 1L );
assertEquals( Double.valueOf( 62.5d ), account.getInterest());
} );
INSERT INTO Account (credit, rate, id)
VALUES (5000.0, 0.0125, 1)
SELECT
a.id as id1_0_0_,
a.credit as credit2_0_0_,
a.rate as rate3_0_0_,
a.credit * a.rate as formula0_0_
FROM
Account a
WHERE
a.id = 1
|
The SQL fragment can be as complex as you want and even include subselects. |
@Where
Sometimes, you want to filter out entities or collections using custom SQL criteria.
This can be achieved using the @Where annotation, which can be applied to entities and collections.
@Where mapping usagepublic enum AccountType {
DEBIT,
CREDIT
}
@Entity(name = "Client")
public static class Client {
@Id
private Long id;
private String name;
@Where( clause = "account_type = 'DEBIT'")
@OneToMany(mappedBy = "client")
private List<Account> debitAccounts = new ArrayList<>( );
@Where( clause = "account_type = 'CREDIT'")
@OneToMany(mappedBy = "client")
private List<Account> creditAccounts = new ArrayList<>( );
//Getters and setters omitted for brevity
}
@Entity(name = "Account")
@Where( clause = "active = true" )
public static class Account {
@Id
private Long id;
@ManyToOne
private Client client;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
private boolean active;
//Getters and setters omitted for brevity
}
If the database contains the following entities:
@Where mappingdoInJPA( this::entityManagerFactory, entityManager -> {
Client client = new Client();
client.setId( 1L );
client.setName( "John Doe" );
entityManager.persist( client );
Account account1 = new Account( );
account1.setId( 1L );
account1.setType( AccountType.CREDIT );
account1.setAmount( 5000d );
account1.setRate( 1.25 / 100 );
account1.setActive( true );
account1.setClient( client );
client.getCreditAccounts().add( account1 );
entityManager.persist( account1 );
Account account2 = new Account( );
account2.setId( 2L );
account2.setType( AccountType.DEBIT );
account2.setAmount( 0d );
account2.setRate( 1.05 / 100 );
account2.setActive( false );
account2.setClient( client );
client.getDebitAccounts().add( account2 );
entityManager.persist( account2 );
Account account3 = new Account( );
account3.setType( AccountType.DEBIT );
account3.setId( 3L );
account3.setAmount( 250d );
account3.setRate( 1.05 / 100 );
account3.setActive( true );
account3.setClient( client );
client.getDebitAccounts().add( account3 );
entityManager.persist( account3 );
} );
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (false, 0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 250.0, 1, 0.0105, 'DEBIT', 3)
When executing an Account entity query, Hibernate is going to filter out all records that are not active.
@WheredoInJPA( this::entityManagerFactory, entityManager -> {
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals( 2, accounts.size());
} );
SELECT
a.id as id1_0_,
a.active as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE ( a.active = true )
When fetching the debitAccounts or the creditAccounts collections, Hibernate is going to apply the @Where clause filtering criteria to the associated child entities.
@WheredoInJPA( this::entityManagerFactory, entityManager -> {
Client client = entityManager.find( Client.class, 1L );
assertEquals( 1, client.getCreditAccounts().size() );
assertEquals( 1, client.getDebitAccounts().size() );
} );
SELECT
c.client_id as client_i6_0_0_,
c.id as id1_0_0_,
c.id as id1_0_1_,
c.active as active2_0_1_,
c.amount as amount3_0_1_,
c.client_id as client_i6_0_1_,
c.rate as rate4_0_1_,
c.account_type as account_5_0_1_
FROM
Account c
WHERE ( c.active = true and c.account_type = 'CREDIT' ) AND c.client_id = 1
SELECT
d.client_id as client_i6_0_0_,
d.id as id1_0_0_,
d.id as id1_0_1_,
d.active as active2_0_1_,
d.amount as amount3_0_1_,
d.client_id as client_i6_0_1_,
d.rate as rate4_0_1_,
d.account_type as account_5_0_1_
FROM
Account d
WHERE ( d.active = true and d.account_type = 'DEBIT' ) AND d.client_id = 1
@WhereJoinTable
Just like @Where annotation, @WhereJoinTable is used to filter out collections using a joined table (e.g. @ManyToMany association).
@WhereJoinTable mapping example@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
@ManyToMany
@JoinTable(
name = "Book_Reader",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "reader_id")
)
@WhereJoinTable( clause = "created_on > DATEADD( 'DAY', -7, CURRENT_TIMESTAMP() )")
private List<Reader> currentWeekReaders = new ArrayList<>( );
//Getters and setters omitted for brevity
}
@Entity(name = "Reader")
public static class Reader {
@Id
private Long id;
private String name;
//Getters and setters omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
title varchar(255),
primary key (id)
)
create table Book_Reader (
book_id bigint not null,
reader_id bigint not null
)
create table Reader (
id bigint not null,
name varchar(255),
primary key (id)
)
alter table Book_Reader
add constraint FKsscixgaa5f8lphs9bjdtpf9g
foreign key (reader_id)
references Reader
alter table Book_Reader
add constraint FKoyrwu9tnwlukd1616qhck21ra
foreign key (book_id)
references Book
alter table Book_Reader
add created_on timestamp
default current_timestamp
In the example above, the current week Reader entities are included in the currentWeekReaders collection
which uses the @WhereJoinTable annotation to filter the joined table rows according to the provided SQL clause.
Considering that the following two Book_Reader entries are added into our system:
@WhereJoinTable test dataBook book = new Book();
book.setId( 1L );
book.setTitle( "High-Performance Java Persistence" );
book.setAuthor( "Vad Mihalcea" );
entityManager.persist( book );
Reader reader1 = new Reader();
reader1.setId( 1L );
reader1.setName( "John Doe" );
entityManager.persist( reader1 );
Reader reader2 = new Reader();
reader2.setId( 2L );
reader2.setName( "John Doe Jr." );
entityManager.persist( reader2 );
statement.executeUpdate(
"INSERT INTO Book_Reader " +
" (book_id, reader_id) " +
"VALUES " +
" (1, 1) "
);
statement.executeUpdate(
"INSERT INTO Book_Reader " +
" (book_id, reader_id, created_on) " +
"VALUES " +
" (1, 2, DATEADD( 'DAY', -10, CURRENT_TIMESTAMP() )) "
);
When fetching the currentWeekReaders collection, Hibernate is going to find one one entry:
@WhereJoinTable fetch exampleBook book = entityManager.find( Book.class, 1L );
assertEquals( 1, book.getCurrentWeekReaders().size() );
@Filter
The @Filter annotation is another way to filter out entities or collections using custom SQL criteria.
Unlike the @Where annotation, @Filter allows you to parameterize the filter clause at runtime.
Now, considering we have the following Account entity:
@Filter mapping entity-level usage@Entity(name = "Account")
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type="boolean"
)
)
@Filter(
name="activeAccount",
condition="active_status = :active"
)
public static class Account {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Client client;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
@Column(name = "active_status")
private boolean active;
//Getters and setters omitted for brevity
}
|
Notice that the This mapping was done to show you that the |
As already explained, we can also apply the @Filter annotation for collections as illustrated by the Client entity:
@Filter mapping collection-level usage@Entity(name = "Client")
public static class Client {
@Id
private Long id;
private String name;
@OneToMany(
mappedBy = "client",
cascade = CascadeType.ALL
)
@Filter(
name="activeAccount",
condition="active_status = :active"
)
private List<Account> accounts = new ArrayList<>( );
//Getters and setters omitted for brevity
public void addAccount(Account account) {
account.setClient( this );
this.accounts.add( account );
}
}
If we persist a Client with three associated Account entities,
Hibernate will execute the following SQL statements:
@Filter mappingClient client = new Client()
.setId( 1L )
.setName( "John Doe" );
client.addAccount(
new Account()
.setId( 1L )
.setType( AccountType.CREDIT )
.setAmount( 5000d )
.setRate( 1.25 / 100 )
.setActive( true )
);
client.addAccount(
new Account()
.setId( 2L )
.setType( AccountType.DEBIT )
.setAmount( 0d )
.setRate( 1.05 / 100 )
.setActive( false )
);
client.addAccount(
new Account()
.setType( AccountType.DEBIT )
.setId( 3L )
.setAmount( 250d )
.setRate( 1.05 / 100 )
.setActive( true )
);
entityManager.persist( client );
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (true, 5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (false, 0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (true, 250.0, 1, 0.0105, 'DEBIT', 3)
By default, without explicitly enabling the filter, Hibernate is going to fetch all Account entities.
@FilterList<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals( 3, accounts.size());
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
If the filter is enabled and the filter parameter value is provided,
then Hibernate is going to apply the filtering criteria to the associated Account entities.
@FilterentityManager
.unwrap( Session.class )
.enableFilter( "activeAccount" )
.setParameter( "active", true);
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals( 2, accounts.size());
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
a.active_status = true
|
Filters apply to entity queries, but not to direct fetching. Therefore, in the following example, the filter is not taken into consideration when fetching an entity from the Persistence Context. Fetching entities mapped with
@Filter
As you can see from the example above, contrary to an entity query, the filter does not prevent the entity from being loaded. |
Just like with entity queries, collections can be filtered as well, but only if the filter is explicitly enabled on the currently running Hibernate Session.
@FilterClient client = entityManager.find( Client.class, 1L );
assertEquals( 3, client.getAccounts().size() );
SELECT
c.id as id1_1_0_,
c.name as name2_1_0_
FROM
Client c
WHERE
c.id = 1
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
a.client_id = 1
When activating the @Filter and fetching the accounts collections, Hibernate is going to apply the filter condition to the associated collection entries.
@FilterentityManager
.unwrap( Session.class )
.enableFilter( "activeAccount" )
.setParameter( "active", true);
Client client = entityManager.find( Client.class, 1L );
assertEquals( 2, client.getAccounts().size() );
SELECT
c.id as id1_1_0_,
c.name as name2_1_0_
FROM
Client c
WHERE
c.id = 1
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
accounts0_.active_status = true
and a.client_id = 1
|
The main advantage of |
|
It’s not possible to combine the If caching were allowed for a currently filtered collection, then the second-level cache would store only a subset of the whole collection. Afterward, every other Session will get the filtered collection from the cache, even if the Session-level filters have not been explicitly activated. For this reason, the second-level collection cache is limited to storing whole collections, and not subsets. |
@FilterJoinTable
When using the @Filter annotation with collections, the filtering is done against the child entries (entities or embeddables).
However, if you have a link table between the parent entity and the child table, then you need to use the @FilterJoinTable to filter child entries according to some column contained in the join table.
The @FilterJoinTable annotation can be, therefore, applied to a unidirectional @OneToMany collection as illustrated in the following mapping:
@FilterJoinTable mapping usage@Entity(name = "Client")
@FilterDef(
name="firstAccounts",
parameters=@ParamDef(
name="maxOrderId",
type="int"
)
)
@Filter(
name="firstAccounts",
condition="order_id <= :maxOrderId"
)
public static class Client {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@FilterJoinTable(
name="firstAccounts",
condition="order_id <= :maxOrderId"
)
private List<Account> accounts = new ArrayList<>( );
//Getters and setters omitted for brevity
public void addAccount(Account account) {
this.accounts.add( account );
}
}
@Entity(name = "Account")
public static class Account {
@Id
private Long id;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
//Getters and setters omitted for brevity
}
The firstAccounts filter will allow us to get only the Account entities that have the order_id
(which tells the position of every entry inside the accounts collection)
less than a given number (e.g. maxOrderId).
Let’s assume our database contains the following entities:
@FilterJoinTable mappingClient client = new Client()
.setId( 1L )
.setName( "John Doe" );
client.addAccount(
new Account()
.setId( 1L )
.setType( AccountType.CREDIT )
.setAmount( 5000d )
.setRate( 1.25 / 100 )
);
client.addAccount(
new Account()
.setId( 2L )
.setType( AccountType.DEBIT )
.setAmount( 0d )
.setRate( 1.05 / 100 )
);
client.addAccount(
new Account()
.setType( AccountType.DEBIT )
.setId( 3L )
.setAmount( 250d )
.setRate( 1.05 / 100 )
);
entityManager.persist( client );
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (250.0, 1, 0.0105, 'DEBIT', 3)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 0, 1)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 0, 1)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 1, 2)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 2, 3)
The collections can be filtered only if the associated filter is enabled on the currently running Hibernate Session.
@FilterJoinTable without enabling the filterClient client = entityManager.find( Client.class, 1L );
assertEquals( 3, client.getAccounts().size());
SELECT
ca.Client_id as Client_i1_2_0_,
ca.accounts_id as accounts2_2_0_,
ca.order_id as order_id3_0_,
a.id as id1_0_1_,
a.amount as amount3_0_1_,
a.rate as rate4_0_1_,
a.account_type as account_5_0_1_
FROM
Client_Account ca
INNER JOIN
Account a
ON ca.accounts_id=a.id
WHERE
ca.Client_id = ?
-- binding parameter [1] as [BIGINT] - [1]
If we enable the filter and set the maxOrderId to 1, when fetching the accounts collections, Hibernate is going to apply the @FilterJoinTable clause filtering criteria, and we will get just
2 Account entities, with the order_id values of 0 and 1.
@FilterJoinTableClient client = entityManager.find( Client.class, 1L );
entityManager
.unwrap( Session.class )
.enableFilter( "firstAccounts" )
.setParameter( "maxOrderId", 1);
assertEquals( 2, client.getAccounts().size());
SELECT
ca.Client_id as Client_i1_2_0_,
ca.accounts_id as accounts2_2_0_,
ca.order_id as order_id3_0_,
a.id as id1_0_1_,
a.amount as amount3_0_1_,
a.rate as rate4_0_1_,
a.account_type as account_5_0_1_
FROM
Client_Account ca
INNER JOIN
Account a
ON ca.accounts_id=a.id
WHERE
ca.order_id <= ?
AND ca.Client_id = ?
-- binding parameter [1] as [INTEGER] - [1]
-- binding parameter [2] as [BIGINT] - [1]
@Filter with @SqlFragmentAlias
When using the @Filter annotation and working with entities that are mapped onto multiple database tables,
you will need to use the
@SqlFragmentAlias annotation
if the @Filter defines a condition that uses predicates across multiple tables.
@SqlFragmentAlias mapping usage@Entity(name = "Account")
@Table(name = "account")
@SecondaryTable(
name = "account_details"
)
@SQLDelete(
sql = "UPDATE account_details SET deleted = true WHERE id = ? "
)
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type="boolean"
)
)
@Filter(
name="activeAccount",
condition="{a}.active = :active and {ad}.deleted = false",
aliases = {
@SqlFragmentAlias( alias = "a", table= "account"),
@SqlFragmentAlias( alias = "ad", table= "account_details"),
}
)
public static class Account {
@Id
private Long id;
private Double amount;
private Double rate;
private boolean active;
@Column(table = "account_details")
private boolean deleted;
//Getters and setters omitted for brevity
}
Now, when fetching the Account entities and activating the filter,
Hibernate is going to apply the right table aliases to the filter predicates:
@SqlFragmentAliasentityManager
.unwrap( Session.class )
.enableFilter( "activeAccount" )
.setParameter( "active", true);
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
select
filtersqlf0_.id as id1_0_,
filtersqlf0_.active as active2_0_,
filtersqlf0_.amount as amount3_0_,
filtersqlf0_.rate as rate4_0_,
filtersqlf0_1_.deleted as deleted1_1_
from
account filtersqlf0_
left outer join
account_details filtersqlf0_1_
on filtersqlf0_.id=filtersqlf0_1_.id
where
filtersqlf0_.active = ?
and filtersqlf0_1_.deleted = false
-- binding parameter [1] as [BOOLEAN] - [true]
@Any mapping
There is one more type of property mapping.
The @Any mapping defines a polymorphic association to classes from multiple tables.
This type of mapping requires more than one column.
The first column contains the type of the associated entity.
The remaining columns contain the identifier.
|
It is impossible to specify a foreign key constraint for this kind of association. This is not the usual way of mapping polymorphic associations and you should use this only in special cases (e.g. audit logs, user session data, etc). |
The @Any annotation describes the column holding the metadata information.
To link the value of the metadata information and an actual entity type, the @AnyDef and @AnyDefs annotations are used.
The metaType attribute allows the application to specify a custom type that maps database column values to persistent classes that have identifier properties of the type specified by idType.
You must specify the mapping from values of the metaType to class names.
For the next examples, consider the following Property class hierarchy:
Property class hierarchypublic interface Property<T> {
String getName();
T getValue();
}
@Entity
@Table(name="integer_property")
public class IntegerProperty implements Property<Integer> {
@Id
private Long id;
@Column(name = "`name`")
private String name;
@Column(name = "`value`")
private Integer value;
@Override
public String getName() {
return name;
}
@Override
public Integer getValue() {
return value;
}
//Getters and setters omitted for brevity
}
@Entity
@Table(name="string_property")
public class StringProperty implements Property<String> {
@Id
private Long id;
@Column(name = "`name`")
private String name;
@Column(name = "`value`")
private String value;
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
//Getters and setters omitted for brevity
}
A PropertyHolder can reference any such property, and, because each Property belongs to a separate table, the @Any annotation is, therefore, required.
@Any mapping usage@Entity
@Table( name = "property_holder" )
public class PropertyHolder {
@Id
private Long id;
@Any(
metaDef = "PropertyMetaDef",
metaColumn = @Column( name = "property_type" )
)
@JoinColumn( name = "property_id" )
private Property property;
//Getters and setters are omitted for brevity
}
CREATE TABLE property_holder (
id BIGINT NOT NULL,
property_type VARCHAR(255),
property_id BIGINT,
PRIMARY KEY ( id )
)
As you can see, there are two columns used to reference a Property instance: property_id and property_type.
The property_id is used to match the id column of either the string_property or integer_property tables,
while the property_type is used to match the string_property or the integer_property table.
The table resolving mapping is defined by the metaDef attribute which references an @AnyMetaDef mapping.
Although the @AnyMetaDef mapping could be set right next to the @Any annotation,
it’s good practice to reuse it, therefore it makes sense to configure it on a class or package-level basis.
The package-info.java contains the @AnyMetaDef mapping:
@Any mapping usage@AnyMetaDef( name= "PropertyMetaDef", metaType = "string", idType = "long",
metaValues = {
@MetaValue(value = "S", targetEntity = StringProperty.class),
@MetaValue(value = "I", targetEntity = IntegerProperty.class)
}
)
package org.hibernate.userguide.mapping.basic.any;
import org.hibernate.annotations.AnyMetaDef;
import org.hibernate.annotations.MetaValue;
|
It is recommended to place the |
To see the @Any annotation in action, consider the next examples.
If we persist an IntegerProperty as well as a StringProperty entity, and associate
the StringProperty entity with a PropertyHolder,
Hibernate will generate the following SQL queries:
@Any mapping persist exampleIntegerProperty ageProperty = new IntegerProperty();
ageProperty.setId( 1L );
ageProperty.setName( "age" );
ageProperty.setValue( 23 );
session.persist( ageProperty );
StringProperty nameProperty = new StringProperty();
nameProperty.setId( 1L );
nameProperty.setName( "name" );
nameProperty.setValue( "John Doe" );
session.persist( nameProperty );
PropertyHolder namePropertyHolder = new PropertyHolder();
namePropertyHolder.setId( 1L );
namePropertyHolder.setProperty( nameProperty );
session.persist( namePropertyHolder );
INSERT INTO integer_property
( "name", "value", id )
VALUES ( 'age', 23, 1 )
INSERT INTO string_property
( "name", "value", id )
VALUES ( 'name', 'John Doe', 1 )
INSERT INTO property_holder
( property_type, property_id, id )
VALUES ( 'S', 1, 1 )
When fetching the PropertyHolder entity and navigating its property association,
Hibernate will fetch the associated StringProperty entity like this:
@Any mapping query examplePropertyHolder propertyHolder = session.get( PropertyHolder.class, 1L );
assertEquals("name", propertyHolder.getProperty().getName());
assertEquals("John Doe", propertyHolder.getProperty().getValue());
SELECT ph.id AS id1_1_0_,
ph.property_type AS property2_1_0_,
ph.property_id AS property3_1_0_
FROM property_holder ph
WHERE ph.id = 1
SELECT sp.id AS id1_2_0_,
sp."name" AS name2_2_0_,
sp."value" AS value3_2_0_
FROM string_property sp
WHERE sp.id = 1
@ManyToAny mapping
The @Any mapping is useful to emulate a @ManyToOne association when there can be multiple target entities.
To emulate a @OneToMany association, the @ManyToAny annotation must be used.
In the following example, the PropertyRepository entity has a collection of Property entities.
The repository_properties link table holds the associations between PropertyRepository and Property entities.
@ManyToAny mapping usage@Entity
@Table( name = "property_repository" )
public class PropertyRepository {
@Id
private Long id;
@ManyToAny(
metaDef = "PropertyMetaDef",
metaColumn = @Column( name = "property_type" )
)
@Cascade( { org.hibernate.annotations.CascadeType.ALL })
@JoinTable(name = "repository_properties",
joinColumns = @JoinColumn(name = "repository_id"),
inverseJoinColumns = @JoinColumn(name = "property_id")
)
private List<Property<?>> properties = new ArrayList<>( );
//Getters and setters are omitted for brevity
}
CREATE TABLE property_repository (
id BIGINT NOT NULL,
PRIMARY KEY ( id )
)
CREATE TABLE repository_properties (
repository_id BIGINT NOT NULL,
property_type VARCHAR(255),
property_id BIGINT NOT NULL
)
To see the @ManyToAny annotation in action, consider the next examples.
If we persist an IntegerProperty as well as a StringProperty entity,
and associate both of them with a PropertyRepository parent entity,
Hibernate will generate the following SQL queries:
@ManyToAny mapping persist exampleIntegerProperty ageProperty = new IntegerProperty();
ageProperty.setId( 1L );
ageProperty.setName( "age" );
ageProperty.setValue( 23 );
session.persist( ageProperty );
StringProperty nameProperty = new StringProperty();
nameProperty.setId( 1L );
nameProperty.setName( "name" );
nameProperty.setValue( "John Doe" );
session.persist( nameProperty );
PropertyRepository propertyRepository = new PropertyRepository();
propertyRepository.setId( 1L );
propertyRepository.getProperties().add( ageProperty );
propertyRepository.getProperties().add( nameProperty );
session.persist( propertyRepository );
INSERT INTO integer_property
( "name", "value", id )
VALUES ( 'age', 23, 1 )
INSERT INTO string_property
( "name", "value", id )
VALUES ( 'name', 'John Doe', 1 )
INSERT INTO property_repository ( id )
VALUES ( 1 )
INSERT INTO repository_properties
( repository_id , property_type , property_id )
VALUES
( 1 , 'I' , 1 )
When fetching the PropertyRepository entity and navigating its properties association,
Hibernate will fetch the associated IntegerProperty and StringProperty entities like this:
@ManyToAny mapping query examplePropertyRepository propertyRepository = session.get( PropertyRepository.class, 1L );
assertEquals(2, propertyRepository.getProperties().size());
for(Property property : propertyRepository.getProperties()) {
assertNotNull( property.getValue() );
}
SELECT pr.id AS id1_1_0_
FROM property_repository pr
WHERE pr.id = 1
SELECT ip.id AS id1_0_0_ ,
ip."name" AS name2_0_0_ ,
ip."value" AS value3_0_0_
FROM integer_property ip
WHERE ip.id = 1
SELECT sp.id AS id1_3_0_ ,
sp."name" AS name2_3_0_ ,
sp."value" AS value3_3_0_
FROM string_property sp
WHERE sp.id = 1
@JoinFormula mapping
The @JoinFormula annotation is used to customize the join between a child Foreign Key and a parent row Primary Key.
@JoinFormula mapping usage@Entity(name = "User")
@Table(name = "users")
public static class User {
@Id
private Long id;
private String firstName;
private String lastName;
private String phoneNumber;
@ManyToOne
@JoinFormula( "REGEXP_REPLACE(phoneNumber, '\\+(\\d+)-.*', '\\1')::int" )
private Country country;
//Getters and setters omitted for brevity
}
@Entity(name = "Country")
@Table(name = "countries")
public static class Country {
@Id
private Integer id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( !( o instanceof Country ) ) {
return false;
}
Country country = (Country) o;
return Objects.equals( getId(), country.getId() );
}
@Override
public int hashCode() {
return Objects.hash( getId() );
}
}
CREATE TABLE countries (
id int4 NOT NULL,
name VARCHAR(255),
PRIMARY KEY ( id )
)
CREATE TABLE users (
id int8 NOT NULL,
firstName VARCHAR(255),
lastName VARCHAR(255),
phoneNumber VARCHAR(255),
PRIMARY KEY ( id )
)
The country association in the User entity is mapped by the country identifier provided by the phoneNumber property.
Considering we have the following entities:
@JoinFormula mapping usageCountry US = new Country();
US.setId( 1 );
US.setName( "United States" );
Country Romania = new Country();
Romania.setId( 40 );
Romania.setName( "Romania" );
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( US );
entityManager.persist( Romania );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
User user1 = new User( );
user1.setId( 1L );
user1.setFirstName( "John" );
user1.setLastName( "Doe" );
user1.setPhoneNumber( "+1-234-5678" );
entityManager.persist( user1 );
User user2 = new User( );
user2.setId( 2L );
user2.setFirstName( "Vlad" );
user2.setLastName( "Mihalcea" );
user2.setPhoneNumber( "+40-123-4567" );
entityManager.persist( user2 );
} );
When fetching the User entities, the country property is mapped by the @JoinFormula expression:
@JoinFormula mapping usagedoInJPA( this::entityManagerFactory, entityManager -> {
log.info( "Fetch User entities" );
User john = entityManager.find( User.class, 1L );
assertEquals( US, john.getCountry());
User vlad = entityManager.find( User.class, 2L );
assertEquals( Romania, vlad.getCountry());
} );
-- Fetch User entities
SELECT
u.id as id1_1_0_,
u.firstName as firstNam2_1_0_,
u.lastName as lastName3_1_0_,
u.phoneNumber as phoneNum4_1_0_,
REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int as formula1_0_,
c.id as id1_0_1_,
c.name as name2_0_1_
FROM
users u
LEFT OUTER JOIN
countries c
ON REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int = c.id
WHERE
u.id=?
-- binding parameter [1] as [BIGINT] - [1]
SELECT
u.id as id1_1_0_,
u.firstName as firstNam2_1_0_,
u.lastName as lastName3_1_0_,
u.phoneNumber as phoneNum4_1_0_,
REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int as formula1_0_,
c.id as id1_0_1_,
c.name as name2_0_1_
FROM
users u
LEFT OUTER JOIN
countries c
ON REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int = c.id
WHERE
u.id=?
-- binding parameter [1] as [BIGINT] - [2]
Therefore, the @JoinFormula annotation is used to define a custom join association between the parent-child association.
@JoinColumnOrFormula mapping
The @JoinColumnOrFormula annotation is used to customize the join between a child Foreign Key and a parent row Primary Key when we need to take into consideration a column value as well as a @JoinFormula.
@JoinColumnOrFormula mapping usage@Entity(name = "User")
@Table(name = "users")
public static class User {
@Id
private Long id;
private String firstName;
private String lastName;
private String language;
@ManyToOne
@JoinColumnOrFormula( column =
@JoinColumn(
name = "language",
referencedColumnName = "primaryLanguage",
insertable = false,
updatable = false
)
)
@JoinColumnOrFormula( formula =
@JoinFormula(
value = "true",
referencedColumnName = "is_default"
)
)
private Country country;
//Getters and setters omitted for brevity
}
@Entity(name = "Country")
@Table(name = "countries")
public static class Country implements Serializable {
@Id
private Integer id;
private String name;
private String primaryLanguage;
@Column(name = "is_default")
private boolean _default;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrimaryLanguage() {
return primaryLanguage;
}
public void setPrimaryLanguage(String primaryLanguage) {
this.primaryLanguage = primaryLanguage;
}
public boolean isDefault() {
return _default;
}
public void setDefault(boolean _default) {
this._default = _default;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( !( o instanceof Country ) ) {
return false;
}
Country country = (Country) o;
return Objects.equals( getId(), country.getId() );
}
@Override
public int hashCode() {
return Objects.hash( getId() );
}
}
CREATE TABLE countries (
id INTEGER NOT NULL,
is_default boolean,
name VARCHAR(255),
primaryLanguage VARCHAR(255),
PRIMARY KEY ( id )
)
CREATE TABLE users (
id BIGINT NOT NULL,
firstName VARCHAR(255),
language VARCHAR(255),
lastName VARCHAR(255),
PRIMARY KEY ( id )
)
The country association in the User entity is mapped by the language property value and the associated Country is_default column value.
Considering we have the following entities:
@JoinColumnOrFormula persist exampleCountry US = new Country();
US.setId( 1 );
US.setDefault( true );
US.setPrimaryLanguage( "English" );
US.setName( "United States" );
Country Romania = new Country();
Romania.setId( 40 );
Romania.setDefault( true );
Romania.setName( "Romania" );
Romania.setPrimaryLanguage( "Romanian" );
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( US );
entityManager.persist( Romania );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
User user1 = new User( );
user1.setId( 1L );
user1.setFirstName( "John" );
user1.setLastName( "Doe" );
user1.setLanguage( "English" );
entityManager.persist( user1 );
User user2 = new User( );
user2.setId( 2L );
user2.setFirstName( "Vlad" );
user2.setLastName( "Mihalcea" );
user2.setLanguage( "Romanian" );
entityManager.persist( user2 );
} );
When fetching the User entities, the country property is mapped by the @JoinColumnOrFormula expression:
@JoinColumnOrFormula fetching exampledoInJPA( this::entityManagerFactory, entityManager -> {
log.info( "Fetch User entities" );
User john = entityManager.find( User.class, 1L );
assertEquals( US, john.getCountry());
User vlad = entityManager.find( User.class, 2L );
assertEquals( Romania, vlad.getCountry());
} );
SELECT
u.id as id1_1_0_,
u.language as language3_1_0_,
u.firstName as firstNam2_1_0_,
u.lastName as lastName4_1_0_,
1 as formula1_0_,
c.id as id1_0_1_,
c.is_default as is_defau2_0_1_,
c.name as name3_0_1_,
c.primaryLanguage as primaryL4_0_1_
FROM
users u
LEFT OUTER JOIN
countries c
ON u.language = c.primaryLanguage
AND 1 = c.is_default
WHERE
u.id = ?
-- binding parameter [1] as [BIGINT] - [1]
SELECT
u.id as id1_1_0_,
u.language as language3_1_0_,
u.firstName as firstNam2_1_0_,
u.lastName as lastName4_1_0_,
1 as formula1_0_,
c.id as id1_0_1_,
c.is_default as is_defau2_0_1_,
c.name as name3_0_1_,
c.primaryLanguage as primaryL4_0_1_
FROM
users u
LEFT OUTER JOIN
countries c
ON u.language = c.primaryLanguage
AND 1 = c.is_default
WHERE
u.id = ?
-- binding parameter [1] as [BIGINT] - [2]
Therefore, the @JoinColumnOrFormula annotation is used to define a custom join association between the parent-child association.
@Target mapping
The @Target annotation is used to specify the implementation class of a given association that is mapped via an interface.
The
@ManyToOne,
@OneToOne,
@OneToMany, and
@ManyToMany
feature a targetEntity attribute to specify the actual class of the entiity association when an interface is used for the mapping.
The @ElementCollection association has a targetClass attribute for the same purpose.
However, for simple embeddable types, there is no such construct and so you need to use the Hibernate-specific @Target annotation instead.
@Target mapping usagepublic interface Coordinates {
double x();
double y();
}
@Embeddable
public static class GPS implements Coordinates {
private double latitude;
private double longitude;
private GPS() {
}
public GPS(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
@Override
public double x() {
return latitude;
}
@Override
public double y() {
return longitude;
}
}
@Entity(name = "City")
public static class City {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
@Target( GPS.class )
private Coordinates coordinates;
//Getters and setters omitted for brevity
}
The coordinates embeddable type is mapped as the Coordinates interface.
However, Hibernate needs to know the actual implementation tye, which is GPS in this case,
hence the @Target annotation is used to provide this information.
Assuming we have persisted the following City entity:
@Target persist exampledoInJPA( this::entityManagerFactory, entityManager -> {
City cluj = new City();
cluj.setName( "Cluj" );
cluj.setCoordinates( new GPS( 46.77120, 23.62360 ) );
entityManager.persist( cluj );
} );
When fetching the City entity, the coordinates property is mapped by the @Target expression:
@Target fetching exampledoInJPA( this::entityManagerFactory, entityManager -> {
City cluj = entityManager.find( City.class, 1L );
assertEquals( 46.77120, cluj.getCoordinates().x(), 0.00001 );
assertEquals( 23.62360, cluj.getCoordinates().y(), 0.00001 );
} );
SELECT
c.id as id1_0_0_,
c.latitude as latitude2_0_0_,
c.longitude as longitud3_0_0_,
c.name as name4_0_0_
FROM
City c
WHERE
c.id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([latitude2_0_0_] : [DOUBLE]) - [46.7712]
-- extracted value ([longitud3_0_0_] : [DOUBLE]) - [23.6236]
-- extracted value ([name4_0_0_] : [VARCHAR]) - [Cluj]
Therefore, the @Target annotation is used to define a custom join association between the parent-child association.
@Parent mapping
The Hibernate-specific @Parent annotation allows you to reference the owner entity from within an embeddable.
@Parent mapping usage@Embeddable
public static class GPS {
private double latitude;
private double longitude;
@Parent
private City city;
private GPS() {
}
public GPS(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
}
@Entity(name = "City")
public static class City {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
@Target( GPS.class )
private GPS coordinates;
//Getters and setters omitted for brevity
}
Assuming we have persisted the following City entity:
@Parent persist exampledoInJPA( this::entityManagerFactory, entityManager -> {
City cluj = new City();
cluj.setName( "Cluj" );
cluj.setCoordinates( new GPS( 46.77120, 23.62360 ) );
entityManager.persist( cluj );
} );
When fetching the City entity, the city property of the embeddable type acts as a back reference to the owning parent entity:
@Parent fetching exampledoInJPA( this::entityManagerFactory, entityManager -> {
City cluj = entityManager.find( City.class, 1L );
assertSame( cluj, cluj.getCoordinates().getCity() );
} );
Therefore, the @Parent annotation is used to define the association between an embeddable type and the owning entity.
Embeddable types
Historically Hibernate called these components. JPA calls them embeddables. Either way the concept is the same: a composition of values.
For example we might have a Publisher class that is a composition of name and country,
or a Location class that is a composition of country and city.
|
Usage of the word embeddable
To avoid any confusion with the annotation that marks a given embeddable type, the annotation will be further referred as Throughout this chapter and thereafter, for brevity sake, embeddable types may also be referred as embeddable. |
@Embeddable
public static class Publisher {
private String name;
private Location location;
public Publisher(String name, Location location) {
this.name = name;
this.location = location;
}
private Publisher() {}
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Location {
private String country;
private String city;
public Location(String country, String city) {
this.country = country;
this.city = city;
}
private Location() {}
//Getters and setters are omitted for brevity
}
An embeddable type is another form of value type, and its lifecycle is bound to a parent entity type, therefore inheriting the attribute access from its parent (for details on attribute access, see Access strategies).
Embeddable types can be made up of basic values as well as associations, with the caveat that, when used as collection elements, they cannot define collections themselves.
Component / Embedded
Most often, embeddable types are used to group multiple basic type mappings and reuse them across several entities.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private Publisher publisher;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Publisher {
@Column(name = "publisher_name")
private String name;
@Column(name = "publisher_country")
private String country;
public Publisher(String name, String country) {
this.name = name;
this.country = country;
}
private Publisher() {}
//Getters and setters are omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
publisher_country varchar(255),
publisher_name varchar(255),
title varchar(255),
primary key (id)
)
|
JPA defines two terms for working with an embeddable type:
|
So, the embeddable type is represented by the Publisher class and
the parent entity makes use of it through the book#publisher object composition.
The composed values are mapped to the same table as the parent table. Composition is part of good Object-oriented data modeling (idiomatic Java). In fact, that table could also be mapped by the following entity type instead.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
@Column(name = "publisher_name")
private String publisherName;
@Column(name = "publisher_country")
private String publisherCountry;
//Getters and setters are omitted for brevity
}
The composition form is certainly more Object-oriented, and that becomes more evident as we work with multiple embeddable types.
Multiple 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 JPA specification demands setting the associated column names explicitly.
This requirement is due to how object properties are mapped to database columns. By default, JPA 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.
We have a few options to handle this issue.
Overriding Embeddable types
JPA 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 Embeddabe type is used multiple times in some entity, you need to use the
@AttributeOverride and
@AssociationOverride annotations
to override the default column names definied by the Embeddable.
Considering you have the following Publisher embeddable type
which defines a @ManyToOne association with the Country entity:
@ManyToOne association@Embeddable
public static class Publisher {
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Country country;
public Publisher(String name, Country country) {
this.name = name;
this.country = country;
}
private Publisher() {}
//Getters and setters are omitted for brevity
}
@Entity(name = "Country")
public static class Country {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
//Getters and setters are omitted for brevity
}
create table Country (
id bigint not null,
name varchar(255),
primary key (id)
)
alter table Country
add constraint UK_p1n05aafu73sbm3ggsxqeditd
unique (name)
Now, if you have a Book entity which declares two Publisher embeddable types for the ebook and paperback version,
you cannot use the default Publisher embeddable mapping since there will be a conflict between the two embeddable column mappings.
Therefore, the Book entity needs to override the embeddable type mappings for each Publisher attribute:
@Entity(name = "Book")
@AttributeOverrides({
@AttributeOverride(
name = "ebookPublisher.name",
column = @Column(name = "ebook_publisher_name")
),
@AttributeOverride(
name = "paperBackPublisher.name",
column = @Column(name = "paper_back_publisher_name")
)
})
@AssociationOverrides({
@AssociationOverride(
name = "ebookPublisher.country",
joinColumns = @JoinColumn(name = "ebook_publisher_country_id")
),
@AssociationOverride(
name = "paperBackPublisher.country",
joinColumns = @JoinColumn(name = "paper_back_publisher_country_id")
)
})
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private Publisher ebookPublisher;
private Publisher paperBackPublisher;
//Getters and setters are omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
ebook_publisher_name varchar(255),
paper_back_publisher_name varchar(255),
title varchar(255),
ebook_publisher_country_id bigint,
paper_back_publisher_country_id bigint,
primary key (id)
)
alter table Book
add constraint FKm39ibh5jstybnslaoojkbac2g
foreign key (ebook_publisher_country_id)
references Country
alter table Book
add constraint FK7kqy9da323p7jw7wvqgs6aek7
foreign key (paper_back_publisher_country_id)
references Country
Embeddables and ImplicitNamingStrategy
|
This is a Hibernate specific feature.
Users concerned with JPA provider portability should instead prefer explicit column naming with |
Hibernate naming strategies are covered in detail in 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.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private Publisher ebookPublisher;
private Publisher paperBackPublisher;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Publisher {
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Country country;
public Publisher(String name, Country country) {
this.name = name;
this.country = country;
}
private Publisher() {}
//Getters and setters are omitted for brevity
}
@Entity(name = "Country")
public static class Country {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
//Getters and setters are omitted for brevity
}
To make it work, you need to use the ImplicitNamingStrategyComponentPathImpl naming strategy.
metadataBuilder.applyImplicitNamingStrategy(
ImplicitNamingStrategyComponentPathImpl.INSTANCE
);
Now the "path" to attributes are used in the implicit column naming:
create table Book (
id bigint not null,
author varchar(255),
ebookPublisher_name varchar(255),
paperBackPublisher_name varchar(255),
title varchar(255),
ebookPublisher_country_id bigint,
paperBackPublisher_country_id bigint,
primary key (id)
)
You could even develop your own naming strategy to do other types of implicit naming strategies.
Collections of embeddable types
Collections of embeddable types are specifically value collections (as embeddable types are a value type). Value collections are covered in detail in Collections of value types.
Embeddable types as Map key
Embeddable types can also be used as Map keys.
This topic is converted in detail in Map - key.
Embeddable types as identifiers
Embeddable types can also be used as entity type identifiers. This usage is covered in detail in Composite identifiers.
|
Embeddable types that are used as collection entries, map keys or entity type identifiers cannot include their own collection mappings. |
Entity types
|
Usage of the word entity
The entity type describes the mapping between the actual persistable domain model object and a database table row.
To avoid any confusion with the annotation that marks a given entity type, the annotation will be further referred as Throughout this chapter and thereafter, entity types will be simply referred as entity. |
POJO Models
Section 2.1 The Entity Class of the JPA 2.1 specification defines its requirements for an entity class. Applications that wish to remain portable across JPA providers should adhere to these requirements.
-
The entity class must be annotated with the
javax.persistence.Entityannotation (or be denoted as such in XML mapping) -
The entity class must have a public or protected no-argument constructor. It may define additional constructors as well.
-
The entity class must be a top-level class.
-
An enum or interface may not be designated as an entity.
-
The entity class must not be final. No methods or persistent instance variables of the entity class may be final.
-
If an entity instance is to be used remotely as a detached object, the entity class must implement the
Serializableinterface. -
Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.
-
The persistent state of an entity is represented by instance variables, which may correspond to JavaBean-style properties. An instance variable must be directly accessed only from within the methods of the entity by the entity instance itself. The state of the entity is available to clients only through the entity’s accessor methods (getter/setter methods) or other business methods.
Hibernate, however, is not as strict in its requirements. The differences from the list above include:
-
The entity class must have a no-argument constructor, which may be public, protected or package visibility. It may define additional constructors as well.
-
The entity class need not be a top-level class.
-
Technically Hibernate can persist final classes or classes with final persistent state accessor (getter/setter) methods. However, it is generally not a good idea as doing so will stop Hibernate from being able to generate proxies for lazy-loading the entity.
-
Hibernate does not restrict the application developer from exposing instance variables and reference them from outside the entity class itself. The validity of such a paradigm, however, is debatable at best.
Let’s look at each requirement in detail.
Prefer non-final classes
A central feature of Hibernate is the ability to load lazily certain entity instance variables (attributes) via runtime proxies. This feature depends upon the entity class being non-final or else implementing an interface that declares all the attribute getters/setters. You can still persist final classes that do not implement such an interface with Hibernate, but you will not be able to use proxies for fetching lazy associations, therefore limiting your options for performance tuning. For the very same reason, you should also avoid declaring persistent attribute getters and setters as final.
|
Starting in 5.0 Hibernate offers a more robust version of bytecode enhancement as another means for handling lazy loading. Hibernate had some bytecode re-writing capabilities prior to 5.0 but they were very rudimentary. See the BytecodeEnhancement for additional information on fetching and on bytecode enhancement. |
Implement a no-argument constructor
The entity class should have a no-argument constructor. Both Hibernate and JPA require this.
JPA requires that this constructor be defined as public or protected. Hibernate, for the most part, does not care about the constructor visibility, as long as the system SecurityManager allows overriding the visibility setting. That said, the constructor should be defined with at least package visibility if you wish to leverage runtime proxy generation.
Declare getters and setters for persistent attributes
The JPA specification requires this, otherwise the model would prevent accessing the entity persistent state fields directly from outside the entity itself.
Although Hibernate does not require it, it is recommended to follow the JavaBean conventions and define getters and setters for entity persistent attributes. Nevertheless, you can still tell Hibernate to directly access the entity fields.
Attributes (whether fields or getters/setters) need not be declared public. Hibernate can deal with attributes declared with public, protected, package or private visibility. Again, if wanting to use runtime proxy generation for lazy loading, the getter/setter should grant access to at least package visibility.
Provide identifier attribute(s)
|
Historically this was considered optional. However, not defining identifier attribute(s) on the entity should be considered a deprecated feature that will be removed in an upcoming release. |
The identifier attribute does not necessarily need to be mapped to the column(s) that physically define the primary key. However, it should map to column(s) that can uniquely identify each row.
|
We recommend that you declare consistently-named identifier attributes on persistent classes and that you use a nullable (i.e., non-primitive) type. |
The placement of the @Id annotation marks the persistence state access strategy.
@Id
private Long id;
Hibernate offers multiple identifier generation strategies, see the Identifier Generators chapter for more about this topic.
Mapping the entity
The main piece in mapping the entity is the javax.persistence.Entity annotation.
The @Entity annotation defines just one attribute name which is used to give a specific entity name for use in JPQL queries.
By default, the entity name represents the unqualified name of the entity class itself.
@Entity mapping@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
An entity models a database table.
The identifier uniquely identifies each row in that table.
By default, the name of the table is assumed to be the same as the name of the entity.
To explicitly give the name of the table or to specify other information about the table, we would use the javax.persistence.Table annotation.
@Entity with @Table@Entity(name = "Book")
@Table(
catalog = "public",
schema = "store",
name = "book"
)
public static class Book {
@Id
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
Implementing equals() and hashCode()
|
Much of the discussion in this section deals with the relation of an entity to a Hibernate Session, whether the entity is managed, transient or detached. If you are unfamiliar with these topics, they are explained in the Persistence Context chapter. |
Whether to implement equals() and hashCode() methods in your domain model, let alone how to implement them, is a surprisingly tricky discussion when it comes to ORM.
There is really just one absolute case: a class that acts as an identifier must implement equals/hashCode based on the id value(s). Generally, this is pertinent for user-defined classes used as composite identifiers. Beyond this one very specific use case and few others we will discuss below, you may want to consider not implementing equals/hashCode altogether.
So what’s all the fuss? Normally, most Java objects provide a built-in equals() and hashCode() based on the object’s identity, so each new object will be different from all others.
This is generally what you want in ordinary Java programming.
Conceptually however this starts to break down when you start to think about the possibility of multiple instances of a class representing the same data.
This is, in fact, exactly the case when dealing with data coming from a database.
Every time we load a specific Person from the database we would naturally get a unique instance.
Hibernate, however, works hard to make sure that does not happen within a given Session.
In fact, Hibernate guarantees equivalence of persistent identity (database row) and Java identity inside a particular session scope.
So if we ask a Hibernate Session to load that specific Person multiple times we will actually get back the same instance:
Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );
assertTrue( book1 == book2 );
Consider we have a Library parent entity which contains a java.util.Set of Book entities:
Library entity mapping
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
Library library = entityManager.find( Library.class, 1L );
Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );
library.getBooks().add( book1 );
library.getBooks().add( book2 );
assertEquals( 1, library.getBooks().size() );
However, the semantic changes when we mix instances loaded from different Sessions:
Book book1 = doInJPA( this::entityManagerFactory, entityManager -> {
return entityManager.find( Book.class, 1L );
} );
Book book2 = doInJPA( this::entityManagerFactory, entityManager -> {
return entityManager.find( Book.class, 1L );
} );
assertFalse( book1 == book2 );
doInJPA( this::entityManagerFactory, entityManager -> {
Set<Book> books = new HashSet<>();
books.add( book1 );
books.add( book2 );
assertEquals( 2, books.size() );
} );
Specifically the outcome in this last example will depend on whether the Book class
implemented equals/hashCode, and, if so, how.
If the Book class did not override the default equals/hashCode,
then the two Book object reference are not going to be equal since their references are different.
Consider yet another case:
Library library = entityManager.find( Library.class, 1L );
Book book1 = new Book();
book1.setId( 100L );
book1.setTitle( "High-Performance Java Persistence" );
Book book2 = new Book();
book2.setId( 101L );
book2.setTitle( "Java Persistence with Hibernate" );
library.getBooks().add( book1 );
library.getBooks().add( book2 );
assertEquals( 2, library.getBooks().size() );
In cases where you will be dealing with entities outside of a Session (whether they be transient or detached), especially in cases where you will be using them in Java collections, you should consider implementing equals/hashCode.
A common initial approach is to use the entity’s identifier attribute as the basis for equals/hashCode calculations:
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Book book = (Book) o;
return Objects.equals( id, book.id );
}
@Override
public int hashCode() {
return Objects.hash( id );
}
}
It turns out that this still breaks when adding transient instance of Book to a set as we saw in the last example:
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );
Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );
Library library = doInJPA( this::entityManagerFactory, entityManager -> {
Library _library = entityManager.find( Library.class, 1L );
_library.getBooks().add( book1 );
_library.getBooks().add( book2 );
return _library;
} );
assertFalse( library.getBooks().contains( book1 ) );
assertFalse( library.getBooks().contains( book2 ) );
The issue here is a conflict between the use of generated identifier, the contract of Set and the equals/hashCode implementations.
Set says that the equals/hashCode value for an object should not change while the object is part of the Set.
But that is exactly what happened here because the equals/hasCode are based on the (generated) id, which was not set until the JPA transaction is committed.
Note that this is just a concern when using generated identifiers.
If you are using assigned identifiers this will not be a problem, assuming the identifier value is assigned prior to adding to the Set.
Another option is to force the identifier to be generated and set prior to adding to the Set:
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );
Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );
Library library = doInJPA( this::entityManagerFactory, entityManager -> {
Library _library = entityManager.find( Library.class, 1L );
entityManager.persist( book1 );
entityManager.persist( book2 );
entityManager.flush();
_library.getBooks().add( book1 );
_library.getBooks().add( book2 );
return _library;
} );
assertTrue( library.getBooks().contains( book1 ) );
assertTrue( library.getBooks().contains( book2 ) );
But this is often not feasible.
The final approach is to use a "better" equals/hashCode implementation, making use of a natural-id or business-key.
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
@NaturalId
private String isbn;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Book book = (Book) o;
return Objects.equals( isbn, book.isbn );
}
@Override
public int hashCode() {
return Objects.hash( isbn );
}
}
This time, when adding a Book to the Library Set, you can retrieve the Book even after it’s being persisted:
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );
book1.setIsbn( "978-9730228236" );
Library library = doInJPA( this::entityManagerFactory, entityManager -> {
Library _library = entityManager.find( Library.class, 1L );
_library.getBooks().add( book1 );
return _library;
} );
assertTrue( library.getBooks().contains( book1 ) );
As you can see the question of equals/hashCode is not trivial, nor is there a one-size-fits-all solution.
|
Although using a natural-id is best for It’s possible to use the entity identifier for equality check, but it needs a workaround:
|
For details on mapping the identifier, see the Identifiers chapter.
Mapping the entity to a SQL query
You can map an entity to a SQL query using the @Subselect annotation.
@Subselect entity mapping@Entity(name = "Client")
@Table(name = "client")
public static class Client {
@Id
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
//Getters and setters omitted for brevity
}
@Entity(name = "Account")
@Table(name = "account")
public static class Account {
@Id
private Long id;
@ManyToOne
private Client client;
private String description;
//Getters and setters omitted for brevity
}
@Entity(name = "AccountTransaction")
@Table(name = "account_transaction")
public static class AccountTransaction {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private Account account;
private Integer cents;
private String description;
//Getters and setters omitted for brevity
}
@Entity(name = "AccountSummary")
@Subselect(
"select " +
" a.id as id, " +
" concat(concat(c.first_name, ' '), c.last_name) as clientName, " +
" sum(at.cents) as balance " +
"from account a " +
"join client c on c.id = a.client_id " +
"join account_transaction at on a.id = at.account_id " +
"group by a.id, concat(concat(c.first_name, ' '), c.last_name)"
)
@Synchronize( {"client", "account", "account_transaction"} )
public static class AccountSummary {
@Id
private Long id;
private String clientName;
private int balance;
//Getters and setters omitted for brevity
}
In the example above, the Account entity does not retain any balance since every account operation is registered as an AccountTransaction.
To find the Account balance, we need to query the AccountSummary which shares the same identifier with the Account entity.
However, the AccountSummary is not mapped to a physical table, but to an SQL query.
So, if we have the following AccountTransaction record, the AccountSummary balance will mach the proper amount of money in this Account.
@Subselect entitydoInJPA( this::entityManagerFactory, entityManager -> {
Client client = new Client();
client.setId( 1L );
client.setFirstName( "John" );
client.setLastName( "Doe" );
entityManager.persist( client );
Account account = new Account();
account.setId( 1L );
account.setClient( client );
account.setDescription( "Checking account" );
entityManager.persist( account );
AccountTransaction transaction = new AccountTransaction();
transaction.setAccount( account );
transaction.setDescription( "Salary" );
transaction.setCents( 100 * 7000 );
entityManager.persist( transaction );
AccountSummary summary = entityManager.createQuery(
"select s " +
"from AccountSummary s " +
"where s.id = :id", AccountSummary.class)
.setParameter( "id", account.getId() )
.getSingleResult();
assertEquals( "John Doe", summary.getClientName() );
assertEquals( 100 * 7000, summary.getBalance() );
} );
If we add a new AccountTransaction entity and refresh the AccountSummary entity, the balance is updated accordingly:
@Subselect entitydoInJPA( this::entityManagerFactory, entityManager -> {
AccountSummary summary = entityManager.find( AccountSummary.class, 1L );
assertEquals( "John Doe", summary.getClientName() );
assertEquals( 100 * 7000, summary.getBalance() );
AccountTransaction transaction = new AccountTransaction();
transaction.setAccount( entityManager.getReference( Account.class, 1L ) );
transaction.setDescription( "Shopping" );
transaction.setCents( -100 * 2200 );
entityManager.persist( transaction );
entityManager.flush();
entityManager.refresh( summary );
assertEquals( 100 * 4800, summary.getBalance() );
} );
|
The goal of the With the |
Define a custom entity proxy
By default, when it needs to use a proxy instead of the actual Pojo, Hibernate is going to use a Bytecode manipulation library like Javassist or Byte Buddy.
However, if the entity class is final, Javassist will not create a proxy and you will get a Pojo even when you only need a proxy reference. In this case, you could proxy an interface that this particular entity implements, as illustrated by the following example.
Identifiable interfacepublic interface Identifiable {
Long getId();
void setId(Long id);
}
@Entity( name = "Book" )
@Proxy(proxyClass = Identifiable.class)
public static final class Book implements Identifiable {
@Id
private Long id;
private String title;
private String author;
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
//Other getters and setters omitted for brevity
}
The @Proxy
annotation is used to specify a custom proxy implementation for the current annotated entity.
When loading the Book entity proxy, Hibernate is going to proxy the Identifiable interface instead as illustrated by the following example:
Identifiable interfacedoInHibernate( this::sessionFactory, session -> {
Book book = new Book();
book.setId( 1L );
book.setTitle( "High-Performance Java Persistence" );
book.setAuthor( "Vlad Mihalcea" );
session.persist( book );
} );
doInHibernate( this::sessionFactory, session -> {
Identifiable book = session.getReference( Book.class, 1L );
assertTrue(
"Loaded entity is not an instance of the proxy interface",
book instanceof Identifiable
);
assertFalse(
"Proxy class was not created",
book instanceof Book
);
} );
insert
into
Book
(author, title, id)
values
(?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Vlad Mihalcea]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [BIGINT] - [1]
As you can see in the associated SQL snippet, Hibernate issues no SQL SELECT query since the proxy can be constructed without needing to fetch the actual entity Pojo.
Dynamic entity proxies using the @Tuplizer annotation
It is possible to map your entities as dynamic proxies using
the @Tuplizer annotation.
In the following entity mapping, both the embeddable and the entity are mapped as interfaces, not Pojos.
@Entity
@Tuplizer(impl = DynamicEntityTuplizer.class)
public interface Cuisine {
@Id
@GeneratedValue
Long getId();
void setId(Long id);
String getName();
void setName(String name);
@Tuplizer(impl = DynamicEmbeddableTuplizer.class)
Country getCountry();
void setCountry(Country country);
}
@Embeddable
public interface Country {
@Column(name = "CountryName")
String getName();
void setName(String name);
}
The @Tuplizer instructs Hibernate to use the DynamicEntityTuplizer and DynamicEmbeddableTuplizer to handle
the associated entity and embeddable object types.
Both the Cuisine entity and the Country embeddable types are going to be instantiated as Java dynamic proxies,
as you can see in the following DynamicInstantiator example:
public class DynamicEntityTuplizer extends PojoEntityTuplizer {
public DynamicEntityTuplizer(
EntityMetamodel entityMetamodel,
PersistentClass mappedEntity) {
super( entityMetamodel, mappedEntity );
}
@Override
protected Instantiator buildInstantiator(
EntityMetamodel entityMetamodel,
PersistentClass persistentClass) {
return new DynamicInstantiator(
persistentClass.getClassName()
);
}
@Override
protected ProxyFactory buildProxyFactory(
PersistentClass persistentClass,
Getter idGetter,
Setter idSetter) {
return super.buildProxyFactory(
persistentClass, idGetter,
idSetter
);
}
}
public class DynamicEmbeddableTuplizer
extends PojoComponentTuplizer {
public DynamicEmbeddableTuplizer(Component embeddable) {
super( embeddable );
}
protected Instantiator buildInstantiator(Component embeddable) {
return new DynamicInstantiator(
embeddable.getComponentClassName()
);
}
}
public class DynamicInstantiator
implements Instantiator {
private final Class targetClass;
public DynamicInstantiator(String targetClassName) {
try {
this.targetClass = Class.forName( targetClassName );
}
catch (ClassNotFoundException e) {
throw new HibernateException( e );
}
}
public Object instantiate(Serializable id) {
return ProxyHelper.newProxy( targetClass, id );
}
public Object instantiate() {
return instantiate( null );
}
public boolean isInstance(Object object) {
try {
return targetClass.isInstance( object );
}
catch( Throwable t ) {
throw new HibernateException(
"could not get handle to entity as interface : " + t
);
}
}
}
public class ProxyHelper {
public static <T> T newProxy(Class<T> targetClass, Serializable id) {
return ( T ) Proxy.newProxyInstance(
targetClass.getClassLoader(),
new Class[] {
targetClass
},
new DataProxyHandler(
targetClass.getName(),
id
)
);
}
public static String extractEntityName(Object object) {
if ( Proxy.isProxyClass( object.getClass() ) ) {
InvocationHandler handler = Proxy.getInvocationHandler(
object
);
if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {
DataProxyHandler myHandler = (DataProxyHandler) handler;
return myHandler.getEntityName();
}
}
return null;
}
}
public final class DataProxyHandler implements InvocationHandler {
private String entityName;
private Map<String, Object> data = new HashMap<>();
public DataProxyHandler(String entityName, Serializable id) {
this.entityName = entityName;
data.put( "Id", id );
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if ( methodName.startsWith( "set" ) ) {
String propertyName = methodName.substring( 3 );
data.put( propertyName, args[0] );
}
else if ( methodName.startsWith( "get" ) ) {
String propertyName = methodName.substring( 3 );
return data.get( propertyName );
}
else if ( "toString".equals( methodName ) ) {
return entityName + "#" + data.get( "Id" );
}
else if ( "hashCode".equals( methodName ) ) {
return this.hashCode();
}
return null;
}
public String getEntityName() {
return entityName;
}
}
With the DynamicInstantiator in place, we can work with the dynamic proxy entities just like with Pojo entities.
Cuisine _cuisine = doInHibernateSessionBuilder(
() -> sessionFactory()
.withOptions()
.interceptor( new EntityNameInterceptor() ),
session -> {
Cuisine cuisine = ProxyHelper.newProxy( Cuisine.class, null );
cuisine.setName( "Française" );
Country country = ProxyHelper.newProxy( Country.class, null );
country.setName( "France" );
cuisine.setCountry( country );
session.persist( cuisine );
return cuisine;
} );
doInHibernateSessionBuilder(
() -> sessionFactory()
.withOptions()
.interceptor( new EntityNameInterceptor() ),
session -> {
Cuisine cuisine = session.get( Cuisine.class, _cuisine.getId() );
assertEquals( "Française", cuisine.getName() );
assertEquals( "France", cuisine.getCountry().getName() );
} );
Define a custom entity persister
The @Persister annotation is used to specify a custom entity or collection persister.
For entities, the custom persister must implement the EntityPersister interface.
For collections, the custom persister must implement the CollectionPersister interface.
@Entity
@Persister( impl = EntityPersister.class )
public class Author {
@Id
public Integer id;
@OneToMany( mappedBy = "author" )
@Persister( impl = CollectionPersister.class )
public Set<Book> books = new HashSet<>();
//Getters and setters omitted for brevity
public void addBook(Book book) {
this.books.add( book );
book.setAuthor( this );
}
}
@Entity
@Persister( impl = EntityPersister.class )
public class Book {
@Id
public Integer id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
public Author author;
//Getters and setters omitted for brevity
}
By providing your own EntityPersister and CollectionPersister implementations,
you can control how entities and collections are persisted in to the database.
Access strategies
As a JPA provider, Hibernate can introspect both the entity attributes (instance fields) or the accessors (instance properties).
By default, the placement of the @Id annotation gives the default access strategy.
When placed on a field, Hibernate will assume field-based access.
When placed on the identifier getter, Hibernate will use property-based access.
|
You should pay attention to Java Beans specification in regard to naming properties to avoid issues such as Property name beginning with at least two uppercase characters has odd functionality in HQL! |
Embeddable types inherit the access strategy from their parent entities.
Field-based access
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
When using field-based access, adding other entity-level methods is much more flexible because Hibernate won’t consider those part of the persistence state.
To exclude a field from being part of the entity persistent state, the field must be marked with the @Transient annotation.
|
Another advantage of using field-based access is that some entity attributes can be hidden from outside the entity.
An example of such attribute is the entity |
Property-based access
@Entity(name = "Book")
public static class Book {
private Long id;
private String title;
private String author;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
When using property-based access, Hibernate uses the accessors for both reading and writing the entity state.
Every other method that will be added to the entity (e.g. helper methods for synchronizing both ends of a bidirectional one-to-many association) will have to be marked with the @Transient annotation.
Overriding the default access strategy
The default access strategy mechanism can be overridden with the JPA @Access annotation.
In the following example, the @Version attribute is accessed by its field and not by its getter, like the rest of entity attributes.
@Entity(name = "Book")
public static class Book {
private Long id;
private String title;
private String author;
@Access( AccessType.FIELD )
@Version
private int version;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
Embeddable types and access strategy
Because embeddables are managed by their owning entities, the access strategy is therefore inherited from the entity too. This applies to both simple embeddable types as well as for collection of embeddables.
The embeddable types can overrule the default implicit access strategy (inherited from the owning entity). In the following example, the embeddable uses property-based access, no matter what access strategy the owning entity is choosing:
@Embeddable
@Access( AccessType.PROPERTY )
public static class Author {
private String firstName;
private String lastName;
public Author() {
}
public Author(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
The owning entity can use field-based access while the embeddable uses property-based access as it has chosen explicitly:
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@Embedded
private Author author;
//Getters and setters are omitted for brevity
}
This works also for collection of embeddable types:
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@ElementCollection
@CollectionTable(
name = "book_author",
joinColumns = @JoinColumn(name = "book_id")
)
private List<Author> authors = new ArrayList<>();
//Getters and setters are omitted for brevity
}
Identifiers
Identifiers model the primary key of an entity. They are used to uniquely identify each specific entity.
Hibernate and JPA both make the following assumptions about the corresponding database column(s):
UNIQUE-
The values must uniquely identify each row.
NOT NULL-
The values cannot be null. For composite ids, no part can be null.
IMMUTABLE-
The values, once inserted, can never be changed. This is more a general guide, than a hard-fast rule as opinions vary. JPA defines the behavior of changing the value of the identifier attribute to be undefined; Hibernate simply does not support that. In cases where the values for the PK you have chosen will be updated, Hibernate recommends mapping the mutable value as a natural id, and use a surrogate id for the PK. See Natural Ids.
|
Technically the identifier does not have to map to the column(s) physically defined as the table primary key. They just need to map to column(s) that uniquely identify each row. However this documentation will continue to use the terms identifier and primary key interchangeably. |
Every entity must define an identifier. For entity inheritance hierarchies, the identifier must be defined just on the entity that is the root of the hierarchy.
An identifier might be simple (single value) or composite (multiple values).
Simple identifiers
Simple identifiers map to a single basic attribute, and are denoted using the javax.persistence.Id annotation.
According to JPA only the following types should be used as identifier attribute types:
-
any Java primitive type
-
any primitive wrapper type
-
java.lang.String -
java.util.Date(TemporalType#DATE) -
java.sql.Date -
java.math.BigDecimal -
java.math.BigInteger
Any types used for identifier attributes beyond this list will not be portable.
Assigned identifiers
Values for simple identifiers can be assigned, as we have seen in the examples above. The expectation for assigned identifier values is that the application assigns (sets them on the entity attribute) prior to calling save/persist.
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
Generated identifiers
Values for simple identifiers can be generated. To denote that an identifier attribute is generated, it is annotated with javax.persistence.GeneratedValue
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
Additionally, to the type restriction list above, JPA says that if using generated identifier values (see below) only integer types (short, int, long) will be portably supported.
The expectation for generated identifier values is that Hibernate will generate the value when the save/persist occurs.
Identifier value generations strategies are discussed in detail in the Generated identifier values section.
Composite identifiers
Composite identifiers correspond to one or more persistent attributes. Here are the rules governing composite identifiers, as defined by the JPA specification.
-
The composite identifier must be represented by a "primary key class". The primary key class may be defined using the
javax.persistence.EmbeddedIdannotation (see Composite identifiers with@EmbeddedId), or defined using thejavax.persistence.IdClassannotation (see Composite identifiers with@IdClass). -
The primary key class must be public and must have a public no-arg constructor.
-
The primary key class must be serializable.
-
The primary key class must define equals and hashCode methods, consistent with equality for the underlying database types to which the primary key is mapped.
|
The restriction that a composite identifier has to be represented by a "primary key class" is only JPA specific. Hibernate does allow composite identifiers to be defined without a "primary key class", although that modeling technique is deprecated and therefore omitted from this discussion. |
The attributes making up the composition can be either basic, composite, ManyToOne. Note especially that collections and one-to-ones are never appropriate.
Composite identifiers with @EmbeddedId
Modeling a composite identifier using an EmbeddedId simply means defining an embeddable to be a composition for the one or more attributes making up the identifier, and then exposing an attribute of that embeddable type on the entity.
@EmbeddedId@Entity(name = "SystemUser")
public static class SystemUser {
@EmbeddedId
private PK pk;
private String name;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class PK implements Serializable {
private String subsystem;
private String username;
public PK(String subsystem, String username) {
this.subsystem = subsystem;
this.username = username;
}
private PK() {
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PK pk = (PK) o;
return Objects.equals( subsystem, pk.subsystem ) &&
Objects.equals( username, pk.username );
}
@Override
public int hashCode() {
return Objects.hash( subsystem, username );
}
}
As mentioned before, EmbeddedIds can even contain @ManyToOne attributes:
@EmbeddedId with @ManyToOne@Entity(name = "SystemUser")
public static class SystemUser {
@EmbeddedId
private PK pk;
private String name;
//Getters and setters are omitted for brevity
}
@Entity(name = "Subsystem")
public static class Subsystem {
@Id
private String id;
private String description;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class PK implements Serializable {
@ManyToOne(fetch = FetchType.LAZY)
private Subsystem subsystem;
private String username;
public PK(Subsystem subsystem, String username) {
this.subsystem = subsystem;
this.username = username;
}
private PK() {
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PK pk = (PK) o;
return Objects.equals( subsystem, pk.subsystem ) &&
Objects.equals( username, pk.username );
}
@Override
public int hashCode() {
return Objects.hash( subsystem, username );
}
}
|
Hibernate supports directly modeling the ManyToOne in the PK class, whether However, that is not portably supported by the JPA specification. In JPA terms one would use "derived identifiers"; for details, see Derived Identifiers. |
Composite identifiers with @IdClass
Modeling a composite identifier using an IdClass differs from using an EmbeddedId in that the entity defines each individual attribute making up the composition. The IdClass simply acts as a "shadow".
@IdClass@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {
@Id
private String subsystem;
@Id
private String username;
private String name;
public PK getId() {
return new PK(
subsystem,
username
);
}
public void setId(PK id) {
this.subsystem = id.getSubsystem();
this.username = id.getUsername();
}
//Getters and setters are omitted for brevity
}
public static class PK implements Serializable {
private String subsystem;
private String username;
public PK(String subsystem, String username) {
this.subsystem = subsystem;
this.username = username;
}
private PK() {
}
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PK pk = (PK) o;
return Objects.equals( subsystem, pk.subsystem ) &&
Objects.equals( username, pk.username );
}
@Override
public int hashCode() {
return Objects.hash( subsystem, username );
}
}
Non-aggregated composite identifiers can also contain ManyToOne attributes as we saw with aggregated ones (still non-portably).
@ManyToOne@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {
@Id
@ManyToOne(fetch = FetchType.LAZY)
private Subsystem subsystem;
@Id
private String username;
private String name;
//Getters and setters are omitted for brevity
}
@Entity(name = "Subsystem")
public static class Subsystem {
@Id
private String id;
private String description;
//Getters and setters are omitted for brevity
}
public static class PK implements Serializable {
private Subsystem subsystem;
private String username;
public PK(Subsystem subsystem, String username) {
this.subsystem = subsystem;
this.username = username;
}
private PK() {
}
//Getters and setters are omitted for brevity
}
With non-aggregated composite identifiers, Hibernate also supports "partial" generation of the composite values.
@IdClass with partial identifier generation using @GeneratedValue@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {
@Id
private String subsystem;
@Id
private String username;
@Id
@GeneratedValue
private Integer registrationId;
private String name;
public PK getId() {
return new PK(
subsystem,
username,
registrationId
);
}
public void setId(PK id) {
this.subsystem = id.getSubsystem();
this.username = id.getUsername();
this.registrationId = id.getRegistrationId();
}
//Getters and setters are omitted for brevity
}
public static class PK implements Serializable {
private String subsystem;
private String username;
private Integer registrationId;
public PK(String subsystem, String username) {
this.subsystem = subsystem;
this.username = username;
}
public PK(String subsystem, String username, Integer registrationId) {
this.subsystem = subsystem;
this.username = username;
this.registrationId = registrationId;
}
private PK() {
}
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PK pk = (PK) o;
return Objects.equals( subsystem, pk.subsystem ) &&
Objects.equals( username, pk.username ) &&
Objects.equals( registrationId, pk.registrationId );
}
@Override
public int hashCode() {
return Objects.hash( subsystem, username, registrationId );
}
}
|
This feature exists because of a highly questionable interpretation of the JPA specification made by the SpecJ committee. Hibernate does not feel that JPA defines support for this, but added the feature simply to be usable in SpecJ benchmarks. Use of this feature may or may not be portable from a JPA perspective. |
Composite identifiers with associations
Hibernate allows defining a composite identifier out of entity associations.
In the following example, the PersonAddress entity identifier is formed of two @ManyToOne associations.
@Entity(name = "Book")
public static class Book implements Serializable {
@Id
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
@Id
@ManyToOne(fetch = FetchType.LAZY)
private Publisher publisher;
@Id
private String title;
public Book(Author author, Publisher publisher, String title) {
this.author = author;
this.publisher = publisher;
this.title = title;
}
private Book() {
}
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Book book = (Book) o;
return Objects.equals( author, book.author ) &&
Objects.equals( publisher, book.publisher ) &&
Objects.equals( title, book.title );
}
@Override
public int hashCode() {
return Objects.hash( author, publisher, title );
}
}
@Entity(name = "Author")
public static class Author implements Serializable {
@Id
private String name;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Author author = (Author) o;
return Objects.equals( name, author.name );
}
@Override
public int hashCode() {
return Objects.hash( name );
}
}
@Entity(name = "Publisher")
public static class Publisher implements Serializable {
@Id
private String name;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Publisher publisher = (Publisher) o;
return Objects.equals( name, publisher.name );
}
@Override
public int hashCode() {
return Objects.hash( name );
}
}
Although the mapping is much simpler than using an @EmbeddedId or an @IdClass, there’s no separation between the entity instance and the actual identifier.
To query this entity, an instance of the entity itself must be supplied to the persistence context.
Book book = entityManager.find( Book.class, new Book(
author,
publisher,
"High-Performance Java Persistence"
) );
assertEquals( "Vlad Mihalcea", book.getAuthor().getName() );
Generated identifier values
|
For discussion of generated values for non-identifier attributes, see Generated properties. |
Hibernate supports identifier value generation across a number of different types. Remember that JPA portably defines identifier value generation just for integer types.
Identifier value generation is indicates using the javax.persistence.GeneratedValue annotation.
The most important piece of information here is the specified javax.persistence.GenerationType which indicates how values will be generated.
|
The discussions below assume that the application is using Hibernate’s "new generator mappings" as indicated by the |
AUTO(the default)-
Indicates that the persistence provider (Hibernate) should choose an appropriate generation strategy. See Interpreting AUTO.
IDENTITY-
Indicates that database IDENTITY columns will be used for primary key value generation. See Using IDENTITY columns.
SEQUENCE-
Indicates that database sequence should be used for obtaining primary key values. See Using sequences.
TABLE-
Indicates that a database table should be used for obtaining primary key values. See Using table identifier generator.
Interpreting AUTO
How a persistence provider interprets the AUTO generation type is left up to the provider.
The default behavior is to look at the java type of the identifier attribute.
If the identifier type is UUID, Hibernate is going to use a UUID identifier.
If the identifier type is numerical (e.g. Long, Integer), then Hibernate is going to use the IdGeneratorStrategyInterpreter to resolve the identifier generator strategy.
The IdGeneratorStrategyInterpreter has two implementations:
FallbackInterpreter-
This is the default strategy since Hibernate 5.0. For older versions, this strategy is enabled through the
hibernate.id.new_generator_mappingsconfiguration property . When using this strategy,AUTOalways resolves toSequenceStyleGenerator. If the underlying database supports sequences, then a SEQUENCE generator is used. Otherwise, a TABLE generator is going to be used instead. LegacyFallbackInterpreter-
This is a legacy mechanism that was used by Hibernate prior to version 5.0 or when the
hibernate.id.new_generator_mappingsconfiguration property is false. The legacy strategy mapsAUTOto thenativegenerator strategy which uses the Dialect#getNativeIdentifierGeneratorStrategy to resolve the actual identifier generator (e.g.identityorsequence).
Using sequences
For implementing database sequence-based identifier value generation Hibernate makes use of its org.hibernate.id.enhanced.SequenceStyleGenerator id generator.
It is important to note that SequenceStyleGenerator is capable of working against databases that do not support sequences by switching to a table as the underlying backing.
This gives Hibernate a huge degree of portability across databases while still maintaining consistent id generation behavior (versus say choosing between SEQUENCE and IDENTITY).
This backing storage is completely transparent to the user.
The preferred (and portable) way to configure this generator is using the JPA-defined javax.persistence.SequenceGenerator annotation.
The simplest form is to simply request sequence generation; Hibernate will use a single, implicitly-named sequence (hibernate_sequence) for all such unnamed definitions.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
Using javax.persistence.SequenceGenerator, you can specify a specific database sequence name.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "sequence-generator"
)
@SequenceGenerator(
name = "sequence-generator",
sequenceName = "product_sequence"
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
The javax.persistence.SequenceGenerator annotataion allows you to specify additional configurations as well.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "sequence-generator"
)
@SequenceGenerator(
name = "sequence-generator",
sequenceName = "product_sequence",
allocationSize = 5
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
Using IDENTITY columns
For implementing identifier value generation based on IDENTITY columns,
Hibernate makes use of its org.hibernate.id.IdentityGenerator id generator which expects the identifier to generated by INSERT into the table.
IdentityGenerator understands 3 different ways that the INSERT-generated value might be retrieved:
-
If Hibernate believes the JDBC environment supports
java.sql.Statement#getGeneratedKeys, then that approach will be used for extracting the IDENTITY generated keys. -
Otherwise, if
Dialect#supportsInsertSelectIdentityreports true, Hibernate will use the Dialect specific INSERT+SELECT statement syntax. -
Otherwise, Hibernate will expect that the database supports some form of asking for the most recently inserted IDENTITY value via a separate SQL command as indicated by
Dialect#getIdentitySelectString.
|
It is important to realize that this imposes a runtime behavior where the entity row must be physically inserted prior to the identifier value being known. This can mess up extended persistence contexts (conversations). Because of the runtime imposition/inconsistency Hibernate suggest other forms of identifier value generation be used. |
|
There is yet another important runtime impact of choosing IDENTITY generation: Hibernate will not be able to JDBC batching for inserts of the entities that use IDENTITY generation. The importance of this depends on the application specific use cases. If the application is not usually creating many new instances of a given type of entity that uses IDENTITY generation, then this is not an important impact since batching would not have been helpful anyway. |
Using table identifier generator
Hibernate achieves table-based identifier generation based on its org.hibernate.id.enhanced.TableGenerator which defines a table capable of holding multiple named value segments for any number of entities.
The basic idea is that a given table-generator table (hibernate_sequences for example) can hold multiple segments of identifier generation values.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
create table hibernate_sequences (
sequence_name varchar2(255 char) not null,
next_val number(19,0),
primary key (sequence_name)
)
If no table name is given Hibernate assumes an implicit name of hibernate_sequences.
Additionally, because no javax.persistence.TableGenerator#pkColumnValue is specified,
Hibernate will use the default segment (sequence_name='default') from the hibernate_sequences table.
However, you can configure the table identifier generator using the @TableGenerator annotation.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator = "table-generator"
)
@TableGenerator(
name = "table-generator",
table = "table_identifier",
pkColumnName = "table_name",
valueColumnName = "product_id",
allocationSize = 5
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
create table table_identifier (
table_name varchar2(255 char) not null,
product_id number(19,0),
primary key (table_name)
)
Now, when inserting 3 Product entities, Hibernate generates the following statements:
for ( long i = 1; i <= 3; i++ ) {
Product product = new Product();
product.setName( String.format( "Product %d", i ) );
entityManager.persist( product );
}
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name = ?
for update
-- binding parameter [1] - [Product]
insert
into
table_identifier
(table_name, product_id)
values
(?, ?)
-- binding parameter [1] - [Product]
-- binding parameter [2] - [1]
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [6]
-- binding parameter [2] - [1]
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name= ? for update
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [11]
-- binding parameter [2] - [6]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [BIGINT] - [1]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [BIGINT] - [2]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [BIGINT] - [3]
Using UUID generation
As mentioned above, Hibernate supports UUID identifier value generation.
This is supported through its org.hibernate.id.UUIDGenerator id generator.
UUIDGenerator supports pluggable strategies for exactly how the UUID is generated.
These strategies are defined by the org.hibernate.id.UUIDGenerationStrategy contract.
The default strategy is a version 4 (random) strategy according to IETF RFC 4122.
Hibernate does ship with an alternative strategy which is a RFC 4122 version 1 (time-based) strategy (using ip address rather than mac address).
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private UUID id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
To specify an alternative generation strategy, we’d have to define some configuration via @GenericGenerator.
Here we choose the RFC 4122 version 1 compliant strategy named org.hibernate.id.uuid.CustomVersionOneStrategy.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue( generator = "custom-uuid" )
@GenericGenerator(
name = "custom-uuid",
strategy = "org.hibernate.id.UUIDGenerator",
parameters = {
@Parameter(
name = "uuid_gen_strategy_class",
value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
)
}
)
private UUID id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
Optimizers
Most of the Hibernate generators that separately obtain identifier values from database structures support the use of pluggable optimizers. Optimizers help manage the number of times Hibernate has to talk to the database in order to generate identifier values. For example, with no optimizer applied to a sequence-generator, every time the application asked Hibernate to generate an identifier it would need to grab the next sequence value from the database. But if we can minimize the number of times we need to communicate with the database here, the application will be able to perform better. Which is, in fact, the role of these optimizers.
- none
-
No optimization is performed. We communicate with the database each and every time an identifier value is needed from the generator.
- pooled-lo
-
The pooled-lo optimizer works on the principle that the increment-value is encoded into the database table/sequence structure. In sequence-terms this means that the sequence is defined with a greater-that-1 increment size.
For example, consider a brand new sequence defined as
create sequence m_sequence start with 1 increment by 20. This sequence essentially defines a "pool" of 20 usable id values each and every time we ask it for its next-value. The pooled-lo optimizer interprets the next-value as the low end of that pool.So when we first ask it for next-value, we’d get 1. We then assume that the valid pool would be the values from 1-20 inclusive.
The next call to the sequence would result in 21, which would define 21-40 as the valid range. And so on. The "lo" part of the name indicates that the value from the database table/sequence is interpreted as the pool lo(w) end.
- pooled
-
Just like pooled-lo, except that here the value from the table/sequence is interpreted as the high end of the value pool.
- hilo; legacy-hilo
-
Define a custom algorithm for generating pools of values based on a single value from a table or sequence.
These optimizers are not recommended for use. They are maintained (and mentioned) here simply for use by legacy applications that used these strategies previously.
|
Applications can also implement and use their own optimizer strategies, as defined by the |
Using @GenericGenerator
@GenericGenerator allows integration of any Hibernate org.hibernate.id.IdentifierGenerator implementation, including any of the specific ones discussed here and any custom ones.
To make use of the pooled or pooled-lo optimizers,
the entity mapping must use the @GenericGenerator annotation:
@GenericGenerator mapping@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "product_generator"
)
@GenericGenerator(
name = "product_generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
@Parameter(name = "sequence_name", value = "product_sequence"),
@Parameter(name = "initial_value", value = "1"),
@Parameter(name = "increment_size", value = "3"),
@Parameter(name = "optimizer", value = "pooled-lo")
}
)
private Long id;
@Column(name = "p_name")
private String name;
@Column(name = "p_number")
private String number;
//Getters and setters are omitted for brevity
}
Now, when saving 5 Person entities and flushing the Persistence Context after every 3 entities:
@GenericGenerator mappingfor ( long i = 1; i <= 5; i++ ) {
if(i % 3 == 0) {
entityManager.flush();
}
Product product = new Product();
product.setName( String.format( "Product %d", i ) );
product.setNumber( String.format( "P_100_%d", i ) );
entityManager.persist( product );
}
CALL NEXT VALUE FOR product_sequence
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [VARCHAR] - [P_100_1]
-- binding parameter [3] as [BIGINT] - [1]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [VARCHAR] - [P_100_2]
-- binding parameter [3] as [BIGINT] - [2]
CALL NEXT VALUE FOR product_sequence
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [VARCHAR] - [P_100_3]
-- binding parameter [3] as [BIGINT] - [3]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 4]
-- binding parameter [2] as [VARCHAR] - [P_100_4]
-- binding parameter [3] as [BIGINT] - [4]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 5]
-- binding parameter [2] as [VARCHAR] - [P_100_5]
-- binding parameter [3] as [BIGINT] - [5]
As you can see from the list of generated SQL statements, you can insert 3 entities for one database sequence call. This way, the pooled and the pooled-lo optimizers allow you to reduce the number of database roundtrips, therefore reducing the overall transaction response time.
Derived Identifiers
JPA 2.0 added support for derived identifiers which allow an entity to borrow the identifier from a many-to-one or one-to-one association.
@MapsId@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@NaturalId
private String registrationNumber;
public Person() {}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PersonDetails")
public static class PersonDetails {
@Id
private Long id;
private String nickName;
@OneToOne
@MapsId
private Person person;
//Getters and setters are omitted for brevity
}
In the example above, the PersonDetails entity uses the id column for both the entity identifier and for the one-to-one association to the Person entity.
The value of the PersonDetails entity identifier is "derived" from the identifier of its parent Person entity.
@MapsId persist exampledoInJPA( this::entityManagerFactory, entityManager -> {
Person person = new Person( "ABC-123" );
person.setId( 1L );
entityManager.persist( person );
PersonDetails personDetails = new PersonDetails();
personDetails.setNickName( "John Doe" );
personDetails.setPerson( person );
entityManager.persist( personDetails );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
PersonDetails personDetails = entityManager.find( PersonDetails.class, 1L );
assertEquals("John Doe", personDetails.getNickName());
} );
The @MapsId annotation can also reference columns from an @EmbeddedId identifier as well.
The previous example can also be mapped using @PrimaryKeyJoinColumn.
@PrimaryKeyJoinColumn@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@NaturalId
private String registrationNumber;
public Person() {}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PersonDetails")
public static class PersonDetails {
@Id
private Long id;
private String nickName;
@OneToOne
@PrimaryKeyJoinColumn
private Person person;
public void setPerson(Person person) {
this.person = person;
this.id = person.getId();
}
//Other getters and setters are omitted for brevity
}
|
Unlike |
@RowId
If you annotate a given entity with the @RowId annotation and the underlying database supports fetching a record by ROWID (e.g. Oracle),
then Hibernate can use the ROWID pseudo-column for CRUD operations.
@RowId entity maapping@Entity(name = "Product")
@RowId("ROWID")
public static class Product {
@Id
private Long id;
@Column(name = "`name`")
private String name;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
Now, when fetching an entity and modifying it, Hibernate uses the ROWID pseudo-column for the UPDATE SQL statement.
@RowId exampleProduct product = entityManager.find( Product.class, 1L );
product.setName( "Smart phone" );
SELECT
p.id as id1_0_0_,
p."name" as name2_0_0_,
p."number" as number3_0_0_,
p.ROWID as rowid_0_
FROM
Product p
WHERE
p.id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([name2_0_0_] : [VARCHAR]) - [Mobile phone]
-- extracted value ([number3_0_0_] : [VARCHAR]) - [123-456-7890]
-- extracted ROWID value: AAAwkBAAEAAACP3AAA
UPDATE
Product
SET
"name" = ?,
"number" = ?
WHERE
ROWID = ?
-- binding parameter [1] as [VARCHAR] - [Smart phone]
-- binding parameter [2] as [VARCHAR] - [123-456-7890]
-- binding parameter [3] as ROWID - [AAAwkBAAEAAACP3AAA]
Associations
Associations describe how two or more entities form a relationship based on a database joining semantics.
@ManyToOne
@ManyToOne is the most common association, having a direct equivalent in the relational database as well (e.g. foreign key),
and so it establishes a relationship between a child entity and a parent.
@ManyToOne association@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
public Person() {
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne
@JoinColumn(name = "person_id",
foreignKey = @ForeignKey(name = "PERSON_ID_FK")
)
private Person person;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT PERSON_ID_FK
FOREIGN KEY (person_id) REFERENCES Person
Each entity has a lifecycle of its own. Once the @ManyToOne association is set, Hibernate will set the associated database foreign key column.
@ManyToOne association lifecyclePerson person = new Person();
entityManager.persist( person );
Phone phone = new Phone( "123-456-7890" );
phone.setPerson( person );
entityManager.persist( phone );
entityManager.flush();
phone.setPerson( null );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, person_id, id )
VALUES ( '123-456-7890', 1, 2 )
UPDATE Phone
SET number = '123-456-7890',
person_id = NULL
WHERE id = 2
@OneToMany
The @OneToMany association links a parent entity with one or more child entities.
If the @OneToMany doesn’t have a mirroring @ManyToOne association on the child side, the @OneToMany association is unidirectional.
If there is a @ManyToOne association on the child side, the @OneToMany association is bidirectional and the application developer can navigate this relationship from both ends.
Unidirectional @OneToMany
When using a unidirectional @OneToMany association, Hibernate resorts to using a link table between the two joining entities.
@OneToMany association@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public List<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)
ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone
ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person
|
The |
@OneToMany associationPerson person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.getPhones().add( phone1 );
person.getPhones().add( phone2 );
entityManager.persist( person );
entityManager.flush();
person.getPhones().remove( phone1 );
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( number, id )
VALUES ( '123-456-7890', 2 )
INSERT INTO Phone
( number, id )
VALUES ( '321-654-0987', 3 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 2 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
DELETE FROM Person_Phone
WHERE Person_id = 1
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
DELETE FROM Phone
WHERE id = 2
When persisting the Person entity, the cascade will propagate the persist operation to the underlying Phone children as well.
Upon removing a Phone from the phones collection, the association row is deleted from the link table, and the orphanRemoval attribute will trigger a Phone removal as well.
|
The unidirectional associations are not very efficient when it comes to removing child entities. In this particular example, upon flushing the persistence context, Hibernate deletes all database child entries and reinserts the ones that are still found in the in-memory persistence context. On the other hand, a bidirectional |
Bidirectional @OneToMany
The bidirectional @OneToMany association also requires a @ManyToOne association on the child side.
Although the Domain Model exposes two sides to navigate this association, behind the scenes, the relational database has only one foreign key for this relationship.
Every bidirectional association must have one owning side only (the child side), the other one being referred to as the inverse (or the mappedBy) side.
@OneToMany association mappedBy the @ManyToOne side@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(name = "`number`", unique = true)
private String number;
@ManyToOne
private Person person;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person
|
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
The |
Because the Phone class has a @NaturalId column (the phone number being unique),
the equals() and the hashCode() can make use of this property, and so the removePhone() logic is reduced to the remove() Java Collection method.
@OneToMany with an owner @ManyToOne side lifecyclePerson person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.addPhone( phone1 );
person.addPhone( phone2 );
entityManager.persist( person );
entityManager.flush();
person.removePhone( phone1 );
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '123-456-7890', 1, 2 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '321-654-0987', 1, 3 )
DELETE FROM Phone
WHERE id = 2
Unlike the unidirectional @OneToMany, the bidirectional association is much more efficient when managing the collection persistence state.
Every element removal only requires a single update (in which the foreign key column is set to NULL), and,
if the child entity lifecycle is bound to its owning parent so that the child cannot exist without its parent,
then we can annotate the association with the orphan-removal attribute and disassociating the child will trigger a delete statement on the actual child table row as well.
@OneToOne
The @OneToOne association can either be unidirectional or bidirectional.
A unidirectional association follows the relational database foreign key semantics, the client-side owning the relationship.
A bidirectional association features a mappedBy @OneToOne parent side too.
Unidirectional @OneToOne
@OneToOne@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne
@JoinColumn(name = "details_id")
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public PhoneDetails getDetails() {
return details;
}
public void setDetails(PhoneDetails details) {
this.details = details;
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
public String getProvider() {
return provider;
}
public String getTechnology() {
return technology;
}
public void setTechnology(String technology) {
this.technology = technology;
}
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
details_id BIGINT ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT FKnoj7cj83ppfqbnvqqa5kolub7
FOREIGN KEY (details_id) REFERENCES PhoneDetails
From a relational database point of view, the underlying schema is identical to the unidirectional @ManyToOne association,
as the client-side controls the relationship based on the foreign key column.
But then, it’s unusual to consider the Phone as a client-side and the PhoneDetails as the parent-side because the details cannot exist without an actual phone.
A much more natural mapping would be if the Phone were the parent-side, therefore pushing the foreign key into the PhoneDetails table.
This mapping requires a bidirectional @OneToOne association as you can see in the following example:
Bidirectional @OneToOne
@OneToOne@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(mappedBy = "phone", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public PhoneDetails getDetails() {
return details;
}
public void addDetails(PhoneDetails details) {
details.setPhone( this );
this.details = details;
}
public void removeDetails() {
if ( details != null ) {
details.setPhone( null );
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
public String getProvider() {
return provider;
}
public String getTechnology() {
return technology;
}
public void setTechnology(String technology) {
this.technology = technology;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
phone_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone
This time, the PhoneDetails owns the association, and, like any bidirectional association, the parent-side can propagate its lifecycle to the child-side through cascading.
@OneToOne lifecyclePhone phone = new Phone( "123-456-7890" );
PhoneDetails details = new PhoneDetails( "T-Mobile", "GSM" );
phone.addDetails( details );
entityManager.persist( phone );
INSERT INTO Phone ( number, id )
VALUES ( '123-456-7890', 1 )
INSERT INTO PhoneDetails ( phone_id, provider, technology, id )
VALUES ( 1, 'T-Mobile', 'GSM', 2 )
When using a bidirectional @OneToOne association, Hibernate enforces the unique constraint upon fetching the child-side.
If there are more than one children associated with the same parent, Hibernate will throw a org.hibernate.exception.ConstraintViolationException.
Continuing the previous example, when adding another PhoneDetails, Hibernate validates the uniqueness constraint when reloading the Phone object.
@OneToOne unique constraintPhoneDetails otherDetails = new PhoneDetails( "T-Mobile", "CDMA" );
otherDetails.setPhone( phone );
entityManager.persist( otherDetails );
entityManager.flush();
entityManager.clear();
//throws javax.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find( Phone.class, phone.getId() );
Bidirectional @OneToOne lazy association
Although you might annotate the parent-side association to be fetched lazily,
Hibernate cannot honor this request since it cannot know whether the association is null or not.
The only way to figure out whether there is an associated record on the child side is to fetch the child association using a secondary query.
Because this can lead to N+1 query issues, it’s much more efficient to use unidirectional @OneToOne associations with the @MapsId annotation in place.
However, if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily,
then you need to enable lazy state initialization bytecode enhancement and use the
@LazyToOne annotation as well.
@OneToOne lazy parent-side association@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(
mappedBy = "phone",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
@LazyToOne( LazyToOneOption.NO_PROXY )
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
//Getters and setters are omitted for brevity
}
For more about how to enable Bytecode enhancement, see the BytecodeEnhancement chapter.
@ManyToMany
The @ManyToMany association requires a link table that joins two entities.
Like the @OneToMany association, @ManyToMany can be a either unidirectional or bidirectional.
Unidirectional @ManyToMany
@ManyToMany@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
public Person() {
}
public List<Address> getAddresses() {
return addresses;
}
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
public Address() {
}
public Address(String street, String number) {
this.street = street;
this.number = number;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
Person_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKba7rc9qe2vh44u93u0p2auwti
FOREIGN KEY (Person_id) REFERENCES Person
Just like with unidirectional @OneToMany associations, the link table is controlled by the owning side.
When an entity is removed from the @ManyToMany collection, Hibernate simply deletes the joining record in the link table.
Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.
@ManyToMany lifecyclePerson person1 = new Person();
Person person2 = new Person();
Address address1 = new Address( "12th Avenue", "12A" );
Address address2 = new Address( "18th Avenue", "18B" );
person1.getAddresses().add( address1 );
person1.getAddresses().add( address2 );
person2.getAddresses().add( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.getAddresses().remove( address1 );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Address ( number, street, id )
VALUES ( '12A', '12th Avenue', 2 )
INSERT INTO Address ( number, street, id )
VALUES ( '18B', '18th Avenue', 3 )
INSERT INTO Person ( id )
VALUES ( 4 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE Person_id = 1
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
|
For For example, if
|
By simply removing the parent-side, Hibernate can safely remove the associated link records as you can see in the following example:
@ManyToMany entity removalPerson person1 = entityManager.find( Person.class, personId );
entityManager.remove( person1 );
DELETE FROM Person_Address
WHERE Person_id = 1
DELETE FROM Person
WHERE id = 1
Bidirectional @ManyToMany
A bidirectional @ManyToMany association has an owning and a mappedBy side.
To preserve synchronicity between both sides, it’s good practice to provide helper methods for adding or removing child entities.
@ManyToMany@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
public Person() {
}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public List<Address> getAddresses() {
return addresses;
}
public void addAddress(Address address) {
addresses.add( address );
address.getOwners().add( this );
}
public void removeAddress(Address address) {
addresses.remove( address );
address.getOwners().remove( this );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@ManyToMany(mappedBy = "addresses")
private List<Person> owners = new ArrayList<>();
public Address() {
}
public Address(String street, String number, String postalCode) {
this.street = street;
this.number = number;
this.postalCode = postalCode;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
public String getPostalCode() {
return postalCode;
}
public List<Person> getOwners() {
return owners;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
owners_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKbn86l24gmxdv2vmekayqcsgup
FOREIGN KEY (owners_id) REFERENCES Person
With the helper methods in place, the synchronicity management can be simplified, as you can see in the following example:
@ManyToMany lifecyclePerson person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.removeAddress( address1 );
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 3 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 4 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE owners_id = 1
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
If a bidirectional @OneToMany association performs better when removing or changing the order of child elements,
the @ManyToMany relationship cannot benefit from such an optimization because the foreign key side is not in control.
To overcome this limitation, the link table must be directly exposed and the @ManyToMany association split into two bidirectional @OneToMany relationships.
Bidirectional many-to-many with a link entity
To most natural @ManyToMany association follows the same logic employed by the database schema,
and the link table has an associated entity which controls the relationship for both sides that need to be joined.
@Entity(name = "Person")
public static class Person implements Serializable {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PersonAddress> addresses = new ArrayList<>();
public Person() {
}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public Long getId() {
return id;
}
public List<PersonAddress> getAddresses() {
return addresses;
}
public void addAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
addresses.add( personAddress );
address.getOwners().add( personAddress );
}
public void removeAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
address.getOwners().remove( personAddress );
addresses.remove( personAddress );
personAddress.setPerson( null );
personAddress.setAddress( null );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "PersonAddress")
public static class PersonAddress implements Serializable {
@Id
@ManyToOne
private Person person;
@Id
@ManyToOne
private Address address;
public PersonAddress() {
}
public PersonAddress(Person person, Address address) {
this.person = person;
this.address = address;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PersonAddress that = (PersonAddress) o;
return Objects.equals( person, that.person ) &&
Objects.equals( address, that.address );
}
@Override
public int hashCode() {
return Objects.hash( person, address );
}
}
@Entity(name = "Address")
public static class Address implements Serializable {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@OneToMany(mappedBy = "address", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PersonAddress> owners = new ArrayList<>();
public Address() {
}
public Address(String street, String number, String postalCode) {
this.street = street;
this.number = number;
this.postalCode = postalCode;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
public String getPostalCode() {
return postalCode;
}
public List<PersonAddress> getOwners() {
return owners;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PersonAddress (
person_id BIGINT NOT NULL ,
address_id BIGINT NOT NULL ,
PRIMARY KEY ( person_id, address_id )
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE PersonAddress
ADD CONSTRAINT FK8b3lru5fyej1aarjflamwghqq
FOREIGN KEY (person_id) REFERENCES Person
ALTER TABLE PersonAddress
ADD CONSTRAINT FK7p69mgialumhegyl4byrh65jk
FOREIGN KEY (address_id) REFERENCES Address
Both the Person and the Address have a mappedBy @OneToMany side, while the PersonAddress owns the person and the address @ManyToOne associations.
Because this mapping is formed out of two bidirectional associations, the helper methods are even more relevant.
|
The aforementioned example uses a Hibernate specific mapping for the link entity since JPA doesn’t allow building a composite identifier out of multiple |
The entity state transitions are better managed than in the previous bidirectional @ManyToMany case.
Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.persist( address1 );
entityManager.persist( address2 );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.flush();
log.info( "Removing address" );
person1.removeAddress( address1 );
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 3 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 3 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 2, 3 )
DELETE FROM PersonAddress
WHERE person_id = 1 AND address_id = 3
There is only one delete statement executed because, this time, the association is controlled by the @ManyToOne side which only has to monitor the state of the underlying foreign key relationship to trigger the right DML statement.
@NotFound association mapping
When dealing with associations which are not enforced by a Foreign Key, it’s possible to bump into inconsistencies if the child record cannot reference a parent entity.
By default, Hibernate will complain whenever a child association references a non-existing parent record.
However, you can configure this behavior so that Hibernate can ignore such an Exception and simply assign null as a parent object referenced.
To ignore non-existing parent entity references, even though not really recommended, it’s possible to use the annotation org.hibernate.annotation.NotFound annotation with a value of org.hibernate.annotations.NotFoundAction.IGNORE.
Considering the following City and Person entity mappings:
@NotFound mapping example@Entity
@Table( name = "Person" )
public static class Person {
@Id
private Long id;
private String name;
private String cityName;
@ManyToOne( fetch = FetchType.LAZY )
@NotFound ( action = NotFoundAction.IGNORE )
@JoinColumn(
name = "cityName",
referencedColumnName = "name",
insertable = false,
updatable = false
)
private City city;
//Getters and setters are omitted for brevity
}
@Entity
@Table( name = "City" )
public static class City implements Serializable {
@Id
@GeneratedValue
private Long id;
private String name;
//Getters and setters are omitted for brevity
}
If we have the following entities in our database:
@NotFound mapping exampleCity _NewYork = new City();
_NewYork.setName( "New York" );
entityManager.persist( _NewYork );
Person person = new Person();
person.setId( 1L );
person.setName( "John Doe" );
person.setCityName( "New York" );
entityManager.persist( person );
When loading the Person entity, Hibernate is able to locate the associated City parent entity:
@NotFound find existing entity examplePerson person = entityManager.find( Person.class, 1L );
assertEquals( "New York", person.getCity().getName() );
However, if we change the cityName attribute to a non-existing city:
@NotFound change to non-existing City exampleperson.setCityName( "Atlantis" );
Hibernate is not going to throw any exception, and it will assign a value of null for the non-existing City entity reference:
@NotFound find non-existing City examplePerson person = entityManager.find( Person.class, 1L );
assertEquals( "Atlantis", person.getCityName() );
assertNull( null, person.getCity() );
Collections
Naturally Hibernate also allows to persist collections. These persistent collections can contain almost any other Hibernate type, including: basic types, custom types, embeddables and references to other entities. In this context, the distinction between value and reference semantics is very important. An object in a collection might be handled with value semantics (its life cycle being fully depends on the collection owner), or it might be a reference to another entity with its own life cycle. In the latter case, only the link between the two objects is considered to be a state held by the collection.
The owner of the collection is always an entity, even if the collection is defined by an embeddable type. Collections form one/many-to-many associations between types so there can be:
-
value type collections
-
embeddable type collections
-
entity collections
Hibernate uses its own collection implementations which are enriched with lazy-loading, caching or state change detection semantics.
For this reason, persistent collections must be declared as an interface type.
The actual interface might be java.util.Collection, java.util.List, java.util.Set, java.util.Map, java.util.SortedSet, java.util.SortedMap or even other object types (meaning you will have to write an implementation of org.hibernate.usertype.UserCollectionType).
As the following example demonstrates, it’s important to use the interface type and not the collection implementation, as declared in the entity mapping.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@ElementCollection
private List<String> phones = new ArrayList<>();
public List<String> getPhones() {
return phones;
}
}
Person person = entityManager.find( Person.class, 1L );
//Throws java.lang.ClassCastException: org.hibernate.collection.internal.PersistentBag cannot be cast to java.util.ArrayList
ArrayList<String> phones = (ArrayList<String>) person.getPhones();
|
It is important that collections be defined using the appropriate Java Collections Framework interface rather than a specific implementation. From a theoretical perspective, this just follows good design principles. From a practical perspective, Hibernate (like other persistence providers) will use their own collection implementations which conform to the Java Collections Framework interfaces. |
The persistent collections injected by Hibernate behave like ArrayList, HashSet, TreeSet, HashMap or TreeMap, depending on the interface type.
Collections as a value type
Value and embeddable type collections have a similar behavior as simple value types because they are automatically persisted when referenced by a persistent object and automatically deleted when unreferenced. If a collection is passed from one persistent object to another, its elements might be moved from one table to another.
|
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. |
Collections of value types
Collections of value type include basic and embeddable types. Collections cannot be nested, and, when used in collections, embeddable types are not allowed to define other collections.
For collections of value types, JPA 2.0 defines the @ElementCollection annotation.
The lifecycle of the value-type collection is entirely controlled by its owning entity.
Considering the previous example mapping, when clearing the phone collection, Hibernate deletes all the associated phones. When adding a new element to the value type collection, Hibernate issues a new insert statement.
person.getPhones().clear();
person.getPhones().add( "123-456-7890" );
person.getPhones().add( "456-000-1234" );
DELETE FROM Person_phones WHERE Person_id = 1
INSERT INTO Person_phones ( Person_id, phones )
VALUES ( 1, '123-456-7890' )
INSERT INTO Person_phones (Person_id, phones)
VALUES ( 1, '456-000-1234' )
If removing all elements or adding new ones is rather straightforward, removing a certain entry actually requires reconstructing the whole collection from scratch.
person.getPhones().remove( 0 );
DELETE FROM Person_phones WHERE Person_id = 1
INSERT INTO Person_phones ( Person_id, phones )
VALUES ( 1, '456-000-1234' )
Depending on the number of elements, this behavior might not be efficient, if many elements need to be deleted and reinserted back into the database table.
A workaround is to use an @OrderColumn, which, although not as efficient as when using the actual link table primary key, might improve the efficiency of the remove operations.
@ElementCollection
@OrderColumn(name = "order_id")
private List<String> phones = new ArrayList<>();
person.getPhones().remove( 0 );
DELETE FROM Person_phones
WHERE Person_id = 1
AND order_id = 1
UPDATE Person_phones
SET phones = '456-000-1234'
WHERE Person_id = 1
AND order_id = 0
|
The |
Embeddable type collections behave the same way as value type collections. Adding embeddables to the collection triggers the associated insert statements and removing elements from the collection will generate delete statements.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@ElementCollection
private List<Phone> phones = new ArrayList<>();
public List<Phone> getPhones() {
return phones;
}
}
@Embeddable
public static class Phone {
private String type;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(String type, String number) {
this.type = type;
this.number = number;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
}
person.getPhones().add( new Phone( "landline", "028-234-9876" ) );
person.getPhones().add( new Phone( "mobile", "072-122-9876" ) );
INSERT INTO Person_phones ( Person_id, number, type )
VALUES ( 1, '028-234-9876', 'landline' )
INSERT INTO Person_phones ( Person_id, number, type )
VALUES ( 1, '072-122-9876', 'mobile' )
Collections of entities
If value type collections can only form a one-to-many association between an owner entity and multiple basic or embeddable types, entity collections can represent both @OneToMany and @ManyToMany associations.
From a relational database perspective, associations are defined by the foreign key side (the child-side). With value type collections, only the entity can control the association (the parent-side), but for a collection of entities, both sides of the association are managed by the persistence context.
For this reason, entity collections can be devised into two main categories: unidirectional and bidirectional associations. Unidirectional associations are very similar to value type collections since only the parent side controls this relationship. Bidirectional associations are more tricky since, even if sides need to be in-sync at all times, only one side is responsible for managing the association. A bidirectional association has an owning side and an inverse (mappedBy) side.
Another way of categorizing entity collections is by the underlying collection type, and so we can have:
-
bags
-
indexed lists
-
sets
-
sorted sets
-
maps
-
sorted maps
-
arrays
In the following sections, we will go through all these collection types and discuss both unidirectional and bidirectional associations.
Bags
Bags are unordered lists and we can have unidirectional bags or bidirectional ones.
Unidirectional bags
The unidirectional bag is mapped using a single @OneToMany annotation on the parent side of the association.
Behind the scenes, Hibernate requires an association table to manage the parent-child relationship, as we can see in the following example:
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
type VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)
ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone
ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person
|
Because both the parent and the child sides are entities, the persistence context manages each entity separately. Cascades can propagate an entity state transition from a parent entity to its children. |
By marking the parent side with the CascadeType.ALL attribute, the unidirectional association lifecycle becomes very similar to that of a value type collection.
Person person = new Person( 1L );
person.getPhones().add( new Phone( 1L, "landline", "028-234-9876" ) );
person.getPhones().add( new Phone( 2L, "mobile", "072-122-9876" ) );
entityManager.persist( person );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, type, id )
VALUES ( '028-234-9876', 'landline', 1 )
INSERT INTO Phone ( number, type, id )
VALUES ( '072-122-9876', 'mobile', 2 )
INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 1 )
INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 2 )
In the example above, once the parent entity is persisted, the child entities are going to be persisted as well.
|
Just like value type collections, unidirectional bags are not as efficient when it comes to modifying the collection structure (removing or reshuffling elements). Because the parent-side cannot uniquely identify each individual child, Hibernate might delete all child table rows associated with the parent entity and re-add them according to the current collection state. |
Bidirectional bags
The bidirectional bag is the most common type of entity collection.
The @ManyToOne side is the owning side of the bidirectional bag association, while the @OneToMany is the inverse side, being marked with the mappedBy attribute.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`", unique = true)
@NaturalId
private String number;
@ManyToOne
private Person person;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
CREATE TABLE Person (
id BIGINT NOT NULL, PRIMARY KEY (id)
)
CREATE TABLE Phone (
id BIGINT NOT NULL,
number VARCHAR(255),
type VARCHAR(255),
person_id BIGINT,
PRIMARY KEY (id)
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEy (person_id) REFERENCES Person
person.addPhone( new Phone( 1L, "landline", "028-234-9876" ) );
person.addPhone( new Phone( 2L, "mobile", "072-122-9876" ) );
entityManager.flush();
person.removePhone( person.getPhones().get( 0 ) );
INSERT INTO Phone (number, person_id, type, id)
VALUES ( '028-234-9876', 1, 'landline', 1 )
INSERT INTO Phone (number, person_id, type, id)
VALUES ( '072-122-9876', 1, 'mobile', 2 )
UPDATE Phone
SET person_id = NULL, type = 'landline' where id = 1
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
DELETE FROM Phone WHERE id = 1
When rerunning the previous example, the child will get removed because the parent-side propagates the removal upon disassociating the child entity reference.
Ordered Lists
Although they use the List interface on the Java side, bags don’t retain element order.
To preserve the collection element order, there are two possibilities:
@OrderBy-
the collection is ordered upon retrieval using a child entity property
@OrderColumn-
the collection uses a dedicated order column in the collection link table
Unidirectional ordered lists
When using the @OrderBy annotation, the mapping looks as follows:
@OrderBy list@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@OrderBy("number")
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
}
The database mapping is the same as with the Unidirectional bags example, so it won’t be repeated. Upon fetching the collection, Hibernate generates the following select statement:
@OrderBy list select statementSELECT
phones0_.Person_id AS Person_i1_1_0_,
phones0_.phones_id AS phones_i2_1_0_,
unidirecti1_.id AS id1_2_1_,
unidirecti1_."number" AS number2_2_1_,
unidirecti1_.type AS type3_2_1_
FROM
Person_Phone phones0_
INNER JOIN
Phone unidirecti1_ ON phones0_.phones_id=unidirecti1_.id
WHERE
phones0_.Person_id = 1
ORDER BY
unidirecti1_."number"
The child table column is used to order the list elements.
|
The If no property is specified (e.g. |
Another ordering option is to use the @OrderColumn annotation:
@OrderColumn list@OneToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
private List<Phone> phones = new ArrayList<>();
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL ,
order_id INTEGER NOT NULL ,
PRIMARY KEY ( Person_id, order_id )
)
This time, the link table takes the order_id column and uses it to materialize the collection element order.
When fetching the list, the following select query is executed:
@OrderColumn list select statementselect
phones0_.Person_id as Person_i1_1_0_,
phones0_.phones_id as phones_i2_1_0_,
phones0_.order_id as order_id3_0_,
unidirecti1_.id as id1_2_1_,
unidirecti1_.number as number2_2_1_,
unidirecti1_.type as type3_2_1_
from
Person_Phone phones0_
inner join
Phone unidirecti1_
on phones0_.phones_id=unidirecti1_.id
where
phones0_.Person_id = 1
With the order_id column in place, Hibernate can order the list in-memory after it’s being fetched from the database.
Bidirectional ordered lists
The mapping is similar with the Bidirectional bags example, just that the parent side is going to be annotated with either @OrderBy or @OrderColumn.
@OrderBy list@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderBy("number")
private List<Phone> phones = new ArrayList<>();
Just like with the unidirectional @OrderBy list, the number column is used to order the statement on the SQL level.
When using the @OrderColumn annotation, the order_id column is going to be embedded in the child table:
@OrderColumn list@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
private List<Phone> phones = new ArrayList<>();
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
type VARCHAR(255) ,
person_id BIGINT ,
order_id INTEGER ,
PRIMARY KEY ( id )
)
When fetching the collection, Hibernate will use the fetched ordered columns to sort the elements according to the @OrderColumn mapping.
Customizing ordered list ordinal
You can customize the ordinal of the underlying ordered list by using the @ListIndexBase annotation.
@ListIndexBase mapping example@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@ListIndexBase(100)
private List<Phone> phones = new ArrayList<>();
When inserting two Phone records, Hibernate is going to start the List index from 100 this time.
@ListIndexBase persist examplePerson person = new Person( 1L );
entityManager.persist( person );
person.addPhone( new Phone( 1L, "landline", "028-234-9876" ) );
person.addPhone( new Phone( 2L, "mobile", "072-122-9876" ) );
INSERT INTO Phone("number", person_id, type, id)
VALUES ('028-234-9876', 1, 'landline', 1)
INSERT INTO Phone("number", person_id, type, id)
VALUES ('072-122-9876', 1, 'mobile', 2)
UPDATE Phone
SET order_id = 100
WHERE id = 1
UPDATE Phone
SET order_id = 101
WHERE id = 2
Customizing ORDER BY SQL clause
While the JPA
@OrderBy annotation allows you to specify the entity attributes used for sorting
when fetching the current annotated collection, the Hibernate specific
@OrderBy annotation is used to specify a SQL clause instead.
In the following example, the @OrderBy annotations uses the CHAR_LENGTH SQL function to order the Article entities
by the number of characters of the name attribute.
@OrderBy mapping example@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@org.hibernate.annotations.OrderBy(clause = "CHAR_LENGTH(name) DESC")
private List<Article> articles = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Article")
public static class Article {
@Id
@GeneratedValue
private Long id;
private String name;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
private Person person;
private Article() {
}
public Article(String name, String content) {
this.name = name;
this.content = content;
}
//Getters and setters are omitted for brevity
}
When fetching the articles collection, Hibernate uses the ORDER BY SQL clause provided by the mapping:
@OrderBy fetching examplePerson person = entityManager.find( Person.class, 1L );
assertEquals(
"High-Performance Hibernate",
person.getArticles().get( 0 ).getName()
);
select
a.person_id as person_i4_0_0_,
a.id as id1_0_0_,
a.content as content2_0_1_,
a.name as name3_0_1_,
a.person_id as person_i4_0_1_
from
Article a
where
a.person_id = ?
order by
CHAR_LENGTH(a.name) desc
Sets
Sets are collections that don’t allow duplicate entries and Hibernate supports both the unordered Set and the natural-ordering SortedSet.
Unidirectional sets
The unidirectional set uses a link table to hold the parent-child associations and the entity mapping looks as follows:
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private Set<Phone> phones = new HashSet<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Set<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@NaturalId
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
The unidirectional set lifecycle is similar to that of the Unidirectional bags, so it can be omitted.
The only difference is that Set doesn’t allow duplicates, but this constraint is enforced by the Java object contract rather than the database mapping.
|
When using sets, it’s very important to supply proper equals/hashCode implementations for child entities. In the absence of a custom equals/hashCode implementation logic, Hibernate will use the default Java reference-based object equality which might render unexpected results when mixing detached and managed object instances. |
Bidirectional sets
Just like bidirectional bags, the bidirectional set doesn’t use a link table, and the child table has a foreign key referencing the parent table primary key. The lifecycle is just like with bidirectional bags except for the duplicates which are filtered out.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private Set<Phone> phones = new HashSet<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Set<Phone> getPhones() {
return phones;
}
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
private String type;
@Column(name = "`number`", unique = true)
@NaturalId
private String number;
@ManyToOne
private Person person;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
Sorted sets
For sorted sets, the entity mapping must use the SortedSet interface instead.
According to the SortedSet contract, all elements must implement the Comparable interface and therefore provide the sorting logic.
Unidirectional sorted sets
A SortedSet that relies on the natural sorting order given by the child element Comparable implementation logic must be annotated with the @SortNatural Hibernate annotation.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@SortNatural
private SortedSet<Phone> phones = new TreeSet<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Set<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {
@Id
private Long id;
private String type;
@NaturalId
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
@Override
public int compareTo(Phone o) {
return number.compareTo( o.getNumber() );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
The lifecycle and the database mapping are identical to the Unidirectional bags, so they are intentionally omitted.
To provide a custom sorting logic, Hibernate also provides a @SortComparator annotation:
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@SortComparator(ReverseComparator.class)
private SortedSet<Phone> phones = new TreeSet<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Set<Phone> getPhones() {
return phones;
}
}
public static class ReverseComparator implements Comparator<Phone> {
@Override
public int compare(Phone o1, Phone o2) {
return o2.compareTo( o1 );
}
}
@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {
@Id
private Long id;
private String type;
@NaturalId
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(Long id, String type, String number) {
this.id = id;
this.type = type;
this.number = number;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getNumber() {
return number;
}
@Override
public int compareTo(Phone o) {
return number.compareTo( o.getNumber() );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
Bidirectional sorted sets
The @SortNatural and @SortComparator work the same for bidirectional sorted sets too:
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@SortNatural
private SortedSet<Phone> phones = new TreeSet<>();
@SortComparator(ReverseComparator.class)
private SortedSet<Phone> phones = new TreeSet<>();
Maps
A java.util.Map is a ternary association because it requires a parent entity, a map key and a value.
An entity can either be a map key or a map value, depending on the mapping.
Hibernate allows using the following map keys:
MapKeyColumn-
for value type maps, the map key is a column in the link table that defines the grouping logic
MapKey-
the map key is either the primary key or another property of the entity stored as a map entry value
MapKeyEnumerated-
the map key is an
Enumof the target child entity MapKeyTemporal-
the map key is a
Dateor aCalendarof the target child entity MapKeyJoinColumn-
the map key is an entity mapped as an association in the child entity that’s stored as a map entry key
Value type maps
A map of value type must use the @ElementCollection annotation, just like value type lists, bags or sets.
public enum PhoneType {
LAND_LINE,
MOBILE
}
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@Temporal(TemporalType.TIMESTAMP)
@ElementCollection
@CollectionTable(name = "phone_register")
@Column(name = "since")
private Map<Phone, Date> phoneRegister = new HashMap<>();
public Person() {}
public Person(Long id) {
this.id = id;
}
public Map<Phone, Date> getPhoneRegister() {
return phoneRegister;
}
}
@Embeddable
public static class Phone {
private PhoneType type;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(PhoneType type, String number) {
this.type = type;
this.number = number;
}
public PhoneType getType() {
return type;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE phone_register (
Person_id BIGINT NOT NULL ,
since TIMESTAMP ,
number VARCHAR(255) NOT NULL ,
type INTEGER NOT NULL ,
PRIMARY KEY ( Person_id, number, type )
)
ALTER TABLE phone_register
ADD CONSTRAINT FKrmcsa34hr68of2rq8qf526mlk
FOREIGN KEY (Person_id) REFERENCES Person
Adding entries to the map generates the following SQL statements:
person.getPhoneRegister().put(
new Phone( PhoneType.LAND_LINE, "028-234-9876" ), new Date()
);
person.getPhoneRegister().put(
new Phone( PhoneType.MOBILE, "072-122-9876" ), new Date()
);
INSERT INTO phone_register (Person_id, number, type, since)
VALUES (1, '072-122-9876', 1, '2015-12-15 17:16:45.311')
INSERT INTO phone_register (Person_id, number, type, since)
VALUES (1, '028-234-9876', 0, '2015-12-15 17:16:45.311')
Maps with a custom key type
Hibernate defines the
@MapKeyType annotation
which you can use to customize the Map key type.
Considering you have the following tables in your database:
create table person (
id int8 not null,
primary key (id)
)
create table call_register (
person_id int8 not null,
phone_number int4,
call_timestamp_epoch int8 not null,
primary key (person_id, call_timestamp_epoch)
)
alter table if exists call_register
add constraint FKsn58spsregnjyn8xt61qkxsub
foreign key (person_id)
references person
The call_register records the call history for every person.
The call_timestamp_epoch column stores the phone call timestamp as a Unix timestamp since epoch.
|
The |
Since we want to map all the calls by their associated java.util.Date, not by their timestamp since epoch which is a number, the entity mapping looks as follows:
@MapKeyType mapping example@Entity
@Table(name = "person")
public static class Person {
@Id
private Long id;
@ElementCollection
@CollectionTable(
name = "call_register",
joinColumns = @JoinColumn(name = "person_id")
)
@MapKeyType(
@Type(
type = "org.hibernate.userguide.collections.type.TimestampEpochType"
)
)
@MapKeyColumn( name = "call_timestamp_epoch" )
@Column(name = "phone_number")
private Map<Date, Integer> callRegister = new HashMap<>();
public void setId(Long id) {
this.id = id;
}
public Map<Date, Integer> getCallRegister() {
return callRegister;
}
}
The associated TimestampEpochType looks as follows:
public class TimestampEpochType
extends AbstractSingleColumnStandardBasicType<Date>
implements VersionType<Date>, LiteralType<Date> {
public static final TimestampEpochType INSTANCE = new TimestampEpochType();
public TimestampEpochType() {
super(
BigIntTypeDescriptor.INSTANCE,
JdbcTimestampTypeDescriptor.INSTANCE
);
}
@Override
public String getName() {
return "epoch";
}
@Override
public Date next(
Date current,
SharedSessionContractImplementor session) {
return seed( session );
}
@Override
public Date seed(
SharedSessionContractImplementor session) {
return new Timestamp( System.currentTimeMillis() );
}
@Override
public Comparator<Date> getComparator() {
return getJavaTypeDescriptor().getComparator();
}
@Override
public String objectToSQLString(
Date value,
Dialect dialect) throws Exception {
final Timestamp ts = Timestamp.class.isInstance( value )
? ( Timestamp ) value
: new Timestamp( value.getTime() );
return StringType.INSTANCE.objectToSQLString(
ts.toString(), dialect
);
}
@Override
public Date fromStringValue(
String xml) throws HibernateException {
return fromString( xml );
}
}
The TimestampEpochType allows us to map a Unix timestamp since epoch to a java.util.Date.
But, without the @MapKeyType Hibernate annotation, it would not be possible to customize the Map key type.
Maps having an interface type as the key
Considering you have the following PhoneNumber interface with an implementation given by the MobilePhone class type:
PhoneNumber interface and the MobilePhone class typepublic interface PhoneNumber {
String get();
}
@Embeddable
public static class MobilePhone
implements PhoneNumber {
static PhoneNumber fromString(String phoneNumber) {
String[] tokens = phoneNumber.split( "-" );
if ( tokens.length != 3 ) {
throw new IllegalArgumentException( "invalid phone number: " + phoneNumber );
}
int i = 0;
return new MobilePhone(
tokens[i++],
tokens[i++],
tokens[i]
);
}
private MobilePhone() {
}
public MobilePhone(
String countryCode,
String operatorCode,
String subscriberCode) {
this.countryCode = countryCode;
this.operatorCode = operatorCode;
this.subscriberCode = subscriberCode;
}
@Column(name = "country_code")
private String countryCode;
@Column(name = "operator_code")
private String operatorCode;
@Column(name = "subscriber_code")
private String subscriberCode;
@Override
public String get() {
return String.format(
"%s-%s-%s",
countryCode,
operatorCode,
subscriberCode
);
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
MobilePhone that = (MobilePhone) o;
return Objects.equals( countryCode, that.countryCode ) &&
Objects.equals( operatorCode, that.operatorCode ) &&
Objects.equals( subscriberCode, that.subscriberCode );
}
@Override
public int hashCode() {
return Objects.hash( countryCode, operatorCode, subscriberCode );
}
}
If you want to use the PhoneNumber interface as a java.util.Map key, then you need to supply the
@MapKeyClass annotation as well.
@MapKeyClass mapping example@Entity
@Table(name = "person")
public static class Person {
@Id
private Long id;
@ElementCollection
@CollectionTable(
name = "call_register",
joinColumns = @JoinColumn(name = "person_id")
)
@MapKeyColumn( name = "call_timestamp_epoch" )
@MapKeyClass( MobilePhone.class )
@Column(name = "call_register")
private Map<PhoneNumber, Integer> callRegister = new HashMap<>();
//Getters and setters are omitted for brevity
}
create table person (
id bigint not null,
primary key (id)
)
create table call_register (
person_id bigint not null,
call_register integer,
country_code varchar(255) not null,
operator_code varchar(255) not null,
subscriber_code varchar(255) not null,
primary key (person_id, country_code, operator_code, subscriber_code)
)
alter table call_register
add constraint FKqyj2at6ik010jqckeaw23jtv2
foreign key (person_id)
references person
When inserting a Person with a callRegister containing 2 MobilePhone references,
Hibernate generates the following SQL statements:
@MapKeyClass persist examplePerson person = new Person();
person.setId( 1L );
person.getCallRegister().put( new MobilePhone( "01", "234", "567" ), 101 );
person.getCallRegister().put( new MobilePhone( "01", "234", "789" ), 102 );
entityManager.persist( person );
insert into person (id) values (?)
-- binding parameter [1] as [BIGINT] - [1]
insert into call_register(
person_id,
country_code,
operator_code,
subscriber_code,
call_register
)
values
(?, ?, ?, ?, ?)
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [VARCHAR] - [01]
-- binding parameter [3] as [VARCHAR] - [234]
-- binding parameter [4] as [VARCHAR] - [789]
-- binding parameter [5] as [INTEGER] - [102]
insert into call_register(
person_id,
country_code,
operator_code,
subscriber_code,
call_register
)
values
(?, ?, ?, ?, ?)
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [VARCHAR] - [01]
-- binding parameter [3] as [VARCHAR] - [234]
-- binding parameter [4] as [VARCHAR] - [567]
-- binding parameter [5] as [INTEGER] - [101]
When fetching a Person and accessing the callRegister Map,
Hibernate generates the following SQL statements:
@MapKeyClass fetch examplePerson person = entityManager.find( Person.class, 1L );
assertEquals( 2, person.getCallRegister().size() );
assertEquals(
Integer.valueOf( 101 ),
person.getCallRegister().get( MobilePhone.fromString( "01-234-567" ) )
);
assertEquals(
Integer.valueOf( 102 ),
person.getCallRegister().get( MobilePhone.fromString( "01-234-789" ) )
);
select
cr.person_id as person_i1_0_0_,
cr.call_register as call_reg2_0_0_,
cr.country_code as country_3_0_,
cr.operator_code as operator4_0_,
cr.subscriber_code as subscrib5_0_
from
call_register cr
where
cr.person_id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([person_i1_0_0_] : [BIGINT]) - [1]
-- extracted value ([call_reg2_0_0_] : [INTEGER]) - [101]
-- extracted value ([country_3_0_] : [VARCHAR]) - [01]
-- extracted value ([operator4_0_] : [VARCHAR]) - [234]
-- extracted value ([subscrib5_0_] : [VARCHAR]) - [567]
-- extracted value ([person_i1_0_0_] : [BIGINT]) - [1]
-- extracted value ([call_reg2_0_0_] : [INTEGER]) - [102]
-- extracted value ([country_3_0_] : [VARCHAR]) - [01]
-- extracted value ([operator4_0_] : [VARCHAR]) - [234]
-- extracted value ([subscrib5_0_] : [VARCHAR]) - [789]
Unidirectional maps
A unidirectional map exposes a parent-child association from the parent-side only.
The following example shows a unidirectional map which also uses a @MapKeyTemporal annotation.
The map key is a timestamp and it’s taken from the child entity table.
|
The |
public enum PhoneType {
LAND_LINE,
MOBILE
}
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinTable(
name = "phone_register",
joinColumns = @JoinColumn(name = "phone_id"),
inverseJoinColumns = @JoinColumn(name = "person_id"))
@MapKey(name = "since")
@MapKeyTemporal(TemporalType.TIMESTAMP)
private Map<Date, Phone> phoneRegister = new HashMap<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Map<Date, Phone> getPhoneRegister() {
return phoneRegister;
}
public void addPhone(Phone phone) {
phoneRegister.put( phone.getSince(), phone );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
private PhoneType type;
@Column(name = "`number`")
private String number;
private Date since;
public Phone() {
}
public Phone(PhoneType type, String number, Date since) {
this.type = type;
this.number = number;
this.since = since;
}
public PhoneType getType() {
return type;
}
public String getNumber() {
return number;
}
public Date getSince() {
return since;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
since TIMESTAMP ,
type INTEGER ,
PRIMARY KEY ( id )
)
CREATE TABLE phone_register (
phone_id BIGINT NOT NULL ,
person_id BIGINT NOT NULL ,
PRIMARY KEY ( phone_id, person_id )
)
ALTER TABLE phone_register
ADD CONSTRAINT FKc3jajlx41lw6clbygbw8wm65w
FOREIGN KEY (person_id) REFERENCES Phone
ALTER TABLE phone_register
ADD CONSTRAINT FK6npoomh1rp660o1b55py9ndw4
FOREIGN KEY (phone_id) REFERENCES Person
Bidirectional maps
Like most bidirectional associations, this relationship is owned by the child-side while the parent is the inverse side and can propagate its own state transitions to the child entities.
In the following example, you can see that @MapKeyEnumerated was used so that the Phone enumeration becomes the map key.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
@MapKey(name = "type")
@MapKeyEnumerated
private Map<PhoneType, Phone> phoneRegister = new HashMap<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public Map<PhoneType, Phone> getPhoneRegister() {
return phoneRegister;
}
public void addPhone(Phone phone) {
phone.setPerson( this );
phoneRegister.put( phone.getType(), phone );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
private PhoneType type;
@Column(name = "`number`")
private String number;
private Date since;
@ManyToOne
private Person person;
public Phone() {
}
public Phone(PhoneType type, String number, Date since) {
this.type = type;
this.number = number;
this.since = since;
}
public PhoneType getType() {
return type;
}
public String getNumber() {
return number;
}
public Date getSince() {
return since;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
since TIMESTAMP ,
type INTEGER ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person
Arrays
When discussing arrays, it is important to understand the distinction between SQL array types and Java arrays that are mapped as part of the application’s domain model.
Not all databases implement the SQL-99 ARRAY type and, for this reason, Hibernate doesn’t support native database array types.
Hibernate does support the mapping of arrays in the Java domain model - conceptually the same as mapping a List. However, it is important to realize that it is impossible for Hibernate to offer lazy-loading for arrays of entities and, for this reason, it is strongly recommended to map a "collection" of entities using a List rather than an array.
Arrays as binary
By default, Hibernate will choose a BINARY type, as supported by the current Dialect.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String[] phones;
public Person() {
}
public Person(Long id) {
this.id = id;
}
public String[] getPhones() {
return phones;
}
public void setPhones(String[] phones) {
this.phones = phones;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
phones VARBINARY(255) ,
PRIMARY KEY ( id )
)
|
If you want to map arrays such as Check out this article for an example of how to write such a custom Hibernate Type. |
Collections as basic value type
Notice how all the previous examples explicitly mark the collection attribute as either ElementCollection, OneToMany or ManyToMany.
Collections not marked as such require a custom Hibernate Type and the collection elements must be stored in a single database column.
This is sometimes beneficial. Consider a use-case such as a VARCHAR column that represents a delimited list/set of Strings.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@Type(type = "comma_delimited_strings")
private List<String> phones = new ArrayList<>();
public List<String> getPhones() {
return phones;
}
}
public class CommaDelimitedStringsJavaTypeDescriptor extends AbstractTypeDescriptor<List> {
public static final String DELIMITER = ",";
public CommaDelimitedStringsJavaTypeDescriptor() {
super(
List.class,
new MutableMutabilityPlan<List>() {
@Override
protected List deepCopyNotNull(List value) {
return new ArrayList( value );
}
}
);
}
@Override
public String toString(List value) {
return ( (List<String>) value ).stream().collect( Collectors.joining( DELIMITER ) );
}
@Override
public List fromString(String string) {
List<String> values = new ArrayList<>();
Collections.addAll( values, string.split( DELIMITER ) );
return values;
}
@Override
public <X> X unwrap(List value, Class<X> type, WrapperOptions options) {
return (X) toString( value );
}
@Override
public <X> List wrap(X value, WrapperOptions options) {
return fromString( (String) value );
}
}
public class CommaDelimitedStringsType extends AbstractSingleColumnStandardBasicType<List> {
public CommaDelimitedStringsType() {
super(
VarcharTypeDescriptor.INSTANCE,
new CommaDelimitedStringsJavaTypeDescriptor()
);
}
@Override
public String getName() {
return "comma_delimited_strings";
}
}
The developer can use the comma-delimited collection like any other collection we’ve discussed so far and Hibernate will take care of the type transformation part. The collection itself behaves like any other basic value type, as its lifecycle is bound to its owner entity.
person.phones.add( "027-123-4567" );
person.phones.add( "028-234-9876" );
session.flush();
person.getPhones().remove( 0 );
INSERT INTO Person ( phones, id )
VALUES ( '027-123-4567,028-234-9876', 1 )
UPDATE Person
SET phones = '028-234-9876'
WHERE id = 1
See the Hibernate Integrations Guide for more details on developing custom value type mappings.
Custom collection types
If you wish to use other collection types than List, Set or Map, like Queue for instance,
you have to use a custom collection type, as illustrated by the following example:
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@CollectionType( type = "org.hibernate.userguide.collections.type.QueueType")
private Collection<Phone> phones = new LinkedList<>();
//Constructors are omitted for brevity
public Queue<Phone> getPhones() {
return (Queue<Phone>) phones;
}
}
@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {
@Id
private Long id;
private String type;
@NaturalId
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
@Override
public int compareTo(Phone o) {
return number.compareTo( o.getNumber() );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
public class QueueType implements UserCollectionType {
@Override
public PersistentCollection instantiate(
SharedSessionContractImplementor session,
CollectionPersister persister) throws HibernateException {
return new PersistentQueue( session );
}
@Override
public PersistentCollection wrap(
SharedSessionContractImplementor session,
Object collection) {
return new PersistentQueue( session, (List) collection );
}
@Override
public Iterator getElementsIterator(Object collection) {
return ( (Queue) collection ).iterator();
}
@Override
public boolean contains(Object collection, Object entity) {
return ( (Queue) collection ).contains( entity );
}
@Override
public Object indexOf(Object collection, Object entity) {
int i = ( (List) collection ).indexOf( entity );
return ( i < 0 ) ? null : i;
}
@Override
public Object replaceElements(
Object original,
Object target,
CollectionPersister persister,
Object owner,
Map copyCache,
SharedSessionContractImplementor session)
throws HibernateException {
Queue result = (Queue) target;
result.clear();
result.addAll( (Queue) original );
return result;
}
@Override
public Object instantiate(int anticipatedSize) {
return new LinkedList<>();
}
}
public class PersistentQueue extends PersistentBag implements Queue {
public PersistentQueue(SharedSessionContractImplementor session) {
super( session );
}
public PersistentQueue(SharedSessionContractImplementor session, List list) {
super( session, list );
}
@Override
public boolean offer(Object o) {
return add(o);
}
@Override
public Object remove() {
return poll();
}
@Override
public Object poll() {
int size = size();
if(size > 0) {
Object first = get(0);
remove( 0 );
return first;
}
throw new NoSuchElementException();
}
@Override
public Object element() {
return peek();
}
@Override
public Object peek() {
return size() > 0 ? get( 0 ) : null;
}
}
|
The reason why the
However, the custom collection type can still be customized as long as the base type is one of the aformentioned persistent types. |
This way, the Phone collection can be used as a java.util.Queue:
Person person = entityManager.find( Person.class, 1L );
Queue<Phone> phones = person.getPhones();
Phone head = phones.peek();
assertSame(head, phones.poll());
assertEquals( 1, phones.size() );
Natural Ids
Natural ids represent domain model unique identifiers that have a meaning in the real world too. Even if a natural id does not make a good primary key (surrogate keys being usually preferred), it’s still useful to tell Hibernate about it. As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by its identifier (PK).
Natural Id Mapping
Natural ids are defined in terms of on e or more persistent attributes.
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
@NaturalId
private String isbn;
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
@NaturalId
@Embedded
private Isbn isbn;
//Getters and setters are omitted for brevity
}
@Embeddable
public static class Isbn implements Serializable {
private String isbn10;
private String isbn13;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Isbn isbn = (Isbn) o;
return Objects.equals( isbn10, isbn.isbn10 ) &&
Objects.equals( isbn13, isbn.isbn13 );
}
@Override
public int hashCode() {
return Objects.hash( isbn10, isbn13 );
}
}
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
@NaturalId
private String productNumber;
@NaturalId
@ManyToOne(fetch = FetchType.LAZY)
private Publisher publisher;
//Getters and setters are omitted for brevity
}
@Entity(name = "Publisher")
public static class Publisher implements Serializable {
@Id
private Long id;
private String name;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Publisher publisher = (Publisher) o;
return Objects.equals( id, publisher.id ) &&
Objects.equals( name, publisher.name );
}
@Override
public int hashCode() {
return Objects.hash( id, name );
}
}
Natural Id API
As stated before, Hibernate provides an API for loading entities by their associate natural id.
This is represented by the org.hibernate.NaturalIdLoadAccess contract obtained via Session#byNaturalId.
|
If the entity does not define a natural id, trying to load an entity by its natural id will throw an exception. |
Book book = entityManager
.unwrap(Session.class)
.byNaturalId( Book.class )
.using( "isbn", "978-9730228236" )
.load();
Book book = entityManager
.unwrap(Session.class)
.byNaturalId( Book.class )
.using(
"isbn",
new Isbn(
"973022823X",
"978-9730228236"
) )
.load();
Book book = entityManager
.unwrap(Session.class)
.byNaturalId( Book.class )
.using("productNumber", "973022823X")
.using("publisher", publisher)
.load();
NaturalIdLoadAccess offers 2 distinct methods for obtaining the entity:
load()-
obtains a reference to the entity, making sure that the entity state is initialized
getReference()-
obtains a reference to the entity. The state may or may not be initialized. If the entity is already associated with the current running Session, that reference (loaded or not) is returned. If the entity is not loaded in the current Session and the entity supports proxy generation, an uninitialized proxy is generated and returned, otherwise the entity is loaded from the database and returned.
NaturalIdLoadAccess allows loading an entity by natural id and at the same time apply a pessimistic lock.
For additional details on locking, see the Locking chapter.
We will discuss the last method available on NaturalIdLoadAccess ( setSynchronizationEnabled() ) in Natural Id - Mutability and Caching.
Because the Company and PostalCarrier entities define "simple" natural ids, we can load them as follows:
Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId( Book.class )
.load( "978-9730228236" );
Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId( Book.class )
.load(
new Isbn(
"973022823X",
"978-9730228236"
)
);
Here we see the use of the org.hibernate.SimpleNaturalIdLoadAccess contract,
obtained via `Session#bySimpleNaturalId().
SimpleNaturalIdLoadAccess is similar to NaturalIdLoadAccess except that it does not define the using method.
Instead, because these simple natural ids are defined based on just one attribute we can directly pass
the corresponding natural id attribute value directly to the load() and getReference() methods.
|
If the entity does not define a natural id, or if the natural id is not of a "simple" type, an exception will be thrown there. |
Natural Id - Mutability and Caching
A natural id may be mutable or immutable. By default the @NaturalId annotation marks an immutable natural id attribute.
An immutable natural id is expected to never change its value.
If the value(s) of the natural id attribute(s) change, @NaturalId(mutable=true) should be used instead.
@Entity(name = "Author")
public static class Author {
@Id
private Long id;
private String name;
@NaturalId(mutable = true)
private String email;
//Getters and setters are omitted for brevity
}
Within the Session, Hibernate maintains a mapping from natural id values to entity identifiers (PK) values. If natural ids values changed, it is possible for this mapping to become out of date until a flush occurs.
To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them when the load() or getReference() methods are executed.
To be clear: this is only pertinent for mutable natural ids.
|
This discovery and adjustment have a performance impact.
If an application is certain that none of its mutable natural ids already associated with the Session have changed, it can disable that checking by calling |
Author author = entityManager
.unwrap(Session.class)
.bySimpleNaturalId( Author.class )
.load( "[email protected]" );
author.setEmail( "[email protected]" );
assertNull(
entityManager
.unwrap(Session.class)
.bySimpleNaturalId( Author.class )
.setSynchronizationEnabled( false )
.load( "[email protected]" )
);
assertSame( author,
entityManager
.unwrap(Session.class)
.bySimpleNaturalId( Author.class )
.setSynchronizationEnabled( true )
.load( "[email protected]" )
);
Not only can this NaturalId-to-PK resolution be cached in the Session, but we can also have it cached in the second-level cache if second level caching is enabled.
@Entity(name = "Book")
@NaturalIdCache
public static class Book {
@Id
private Long id;
private String title;
private String author;
@NaturalId
private String isbn;
//Getters and setters are omitted for brevity
}
Dynamic Model
|
JPA only acknowledges the entity model mapping so, if you are concerned about JPA provider portability, it’s best to stick to the strict POJO model. On the other hand, Hibernate can work with both POJO entities as well as with dynamic entity models. |
Dynamic mapping models
Persistent entities do not necessarily have to be represented as POJO/JavaBean classes.
Hibernate also supports dynamic models (using Map of Maps at runtime).
With this approach, you do not write persistent classes, only mapping files.
A given entity has just one entity mode within a given SessionFactory. This is a change from previous versions which allowed to define multiple entity modes for an entity and to select which to load. Entity modes can now be mixed within a domain model; a dynamic entity might reference a POJO entity, and vice versa.
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class entity-name="Book">
<id name="isbn" column="isbn" length="32" type="string"/>
<property name="title" not-null="true" length="50" type="string"/>
<property name="author" not-null="true" length="50" type="string"/>
</class>
</hibernate-mapping>
After you defined your entity mapping, you need to instruct Hibernate to use the dynamic mapping mode:
settings.put( "hibernate.default_entity_mode", "dynamic-map" );
When you are going to save the following Book dynamic entity,
Hibernate is going to generate the following SQL statement:
Map<String, String> book = new HashMap<>();
book.put( "isbn", "978-9730228236" );
book.put( "title", "High-Performance Java Persistence" );
book.put( "author", "Vlad Mihalcea" );
entityManager
.unwrap(Session.class)
.save( "Book", book );
insert
into
Book
(title, author, isbn)
values
(?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [2] as [VARCHAR] - [Vlad Mihalcea]
-- binding parameter [3] as [VARCHAR] - [978-9730228236]
|
The main advantage of dynamic models is quick turnaround time for prototyping without the need for entity class implementation. The main down-fall is that you lose compile-time type checking and will likely deal with many exceptions at runtime. However, as a result of the Hibernate mapping, the database schema can easily be normalized and sound, allowing to add a proper domain model implementation on top later on. It is also interesting to note that dynamic models are great for certain integration use cases as well. Envers, for example, makes extensive use of dynamic models to represent the historical data. |
Inheritance
Although relational database systems don’t provide support for inheritance, Hibernate provides several strategies to leverage this object-oriented trait onto domain model entities:
- MappedSuperclass
-
Inheritance is implemented in domain model only without reflecting it in the database schema. See MappedSuperclass.
- Single table
-
The domain model class hierarchy is materialized into a single table which contains entities belonging to different class types. See Single table.
- Joined table
-
The base class and all the subclasses have their own database tables and fetching a subclass entity requires a join with the parent table as well. See Joined table.
- Table per class
-
Each subclass has its own table containing both the subclass and the base class properties. See Table per class.
MappedSuperclass
In the following domain model class hierarchy, a 'DebitAccount' and a 'CreditAccount' share the same 'Account' base class.
When using MappedSuperclass, the inheritance is visible in the domain model only and each database table contains both the base class and the subclass properties.
@MappedSuperclass inheritance@MappedSuperclass
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE DebitAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
overdraftFee NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
CREATE TABLE CreditAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
creditLimit NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
|
Because the |
Single table
The single table inheritance strategy maps all subclasses to only one database table. Each subclass declares its own persistent properties. Version and id properties are assumed to be inherited from the root class.
|
When omitting an explicit inheritance strategy (e.g. |
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE Account (
DTYPE VARCHAR(31) NOT NULL ,
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
overdraftFee NUMERIC(19, 2) ,
creditLimit NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
Each subclass in a hierarchy must define a unique discriminator value, which is used to differentiate between rows belonging to separate subclass types.
If this is not specified, the DTYPE column is used as a discriminator, storing the associated subclass name.
DebitAccount debitAccount = new DebitAccount();
debitAccount.setId( 1L );
debitAccount.setOwner( "John Doe" );
debitAccount.setBalance( BigDecimal.valueOf( 100 ) );
debitAccount.setInterestRate( BigDecimal.valueOf( 1.5d ) );
debitAccount.setOverdraftFee( BigDecimal.valueOf( 25 ) );
CreditAccount creditAccount = new CreditAccount();
creditAccount.setId( 2L );
creditAccount.setOwner( "John Doe" );
creditAccount.setBalance( BigDecimal.valueOf( 1000 ) );
creditAccount.setInterestRate( BigDecimal.valueOf( 1.9d ) );
creditAccount.setCreditLimit( BigDecimal.valueOf( 5000 ) );
entityManager.persist( debitAccount );
entityManager.persist( creditAccount );
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'DebitAccount', 1)
INSERT INTO Account (balance, interestRate, owner, creditLimit, DTYPE, id)
VALUES (1000, 1.9, 'John Doe', 5000, 'CreditAccount', 2)
When using polymorphic queries, only a single table is required to be scanned to fetch all associated subclass instances.
List<Account> accounts = entityManager
.createQuery( "select a from Account a" )
.getResultList();
SELECT singletabl0_.id AS id2_0_ ,
singletabl0_.balance AS balance3_0_ ,
singletabl0_.interestRate AS interest4_0_ ,
singletabl0_.owner AS owner5_0_ ,
singletabl0_.overdraftFee AS overdraf6_0_ ,
singletabl0_.creditLimit AS creditLi7_0_ ,
singletabl0_.DTYPE AS DTYPE1_0_
FROM Account singletabl0_
|
Among all other inheritance alternatives, the single table strategy performs the best since it requires access to one table only.
Because all subclass columns are stored in a single table, it’s not possible to use NOT NULL constraints anymore, so integrity checks must be moved either into the data access layer or enforced through |
Discriminator
The discriminator column contains marker values that tell the persistence layer what subclass to instantiate for a particular row.
Hibernate Core supports the following restricted set of types as discriminator column: String, char, int, byte, short, boolean(including yes_no, true_false).
Use the @DiscriminatorColumn to define the discriminator column as well as the discriminator type.
|
The enum
The The second option, |
|
There used to be |
Discriminator formula
Assuming a legacy database schema where the discriminator is based on inspecting a certain column,
we can take advantage of the Hibernate specific @DiscriminatorFormula annotation and map the inheritance model as follows:
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula(
"case when debitKey is not null " +
"then 'Debit' " +
"else ( " +
" case when creditKey is not null " +
" then 'Credit' " +
" else 'Unknown' " +
" end ) " +
"end "
)
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
@DiscriminatorValue(value = "Debit")
public static class DebitAccount extends Account {
private String debitKey;
private BigDecimal overdraftFee;
private DebitAccount() {
}
public DebitAccount(String debitKey) {
this.debitKey = debitKey;
}
public String getDebitKey() {
return debitKey;
}
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
@DiscriminatorValue(value = "Credit")
public static class CreditAccount extends Account {
private String creditKey;
private BigDecimal creditLimit;
private CreditAccount() {
}
public CreditAccount(String creditKey) {
this.creditKey = creditKey;
}
public String getCreditKey() {
return creditKey;
}
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE Account (
id int8 NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
debitKey VARCHAR(255) ,
overdraftFee NUMERIC(19, 2) ,
creditKey VARCHAR(255) ,
creditLimit NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
The @DiscriminatorFormula defines a custom SQL clause that can be used to identify a certain subclass type.
The @DiscriminatorValue defines the mapping between the result of the @DiscriminatorFormula and the inheritance subclass type.
Implicit discriminator values
Aside from the usual discriminator values assigned to each individual subclass type, the @DiscriminatorValue can take two additional values:
null-
If the underlying discriminator column is null, the
nulldiscriminator mapping is going to be used. not null-
If the underlying discriminator column has a not-null value that is not explicitly mapped to any entity, the
not-nulldiscriminator mapping used.
To understand how these two values work, consider the following entity mapping:
null and not-null entity mapping@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue( "null" )
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
@DiscriminatorValue( "Debit" )
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
@DiscriminatorValue( "Credit" )
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
@Entity(name = "OtherAccount")
@DiscriminatorValue( "not null" )
public static class OtherAccount extends Account {
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
The Account class has a @DiscriminatorValue( "null" ) mapping, meaning that any account row which does not contain any discriminator value will be mapped to an Account base class entity.
The DebitAccount and CreditAccount entities use explicit discriminator values.
The OtherAccount entity is used as a generic account type because it maps any database row whose discriminator column is not explicitly assigned to any other entity in the current inheritance tree.
To visualize how it works, consider the following example:
null and not-null entity persistenceDebitAccount debitAccount = new DebitAccount();
debitAccount.setId( 1L );
debitAccount.setOwner( "John Doe" );
debitAccount.setBalance( BigDecimal.valueOf( 100 ) );
debitAccount.setInterestRate( BigDecimal.valueOf( 1.5d ) );
debitAccount.setOverdraftFee( BigDecimal.valueOf( 25 ) );
CreditAccount creditAccount = new CreditAccount();
creditAccount.setId( 2L );
creditAccount.setOwner( "John Doe" );
creditAccount.setBalance( BigDecimal.valueOf( 1000 ) );
creditAccount.setInterestRate( BigDecimal.valueOf( 1.9d ) );
creditAccount.setCreditLimit( BigDecimal.valueOf( 5000 ) );
Account account = new Account();
account.setId( 3L );
account.setOwner( "John Doe" );
account.setBalance( BigDecimal.valueOf( 1000 ) );
account.setInterestRate( BigDecimal.valueOf( 1.9d ) );
entityManager.persist( debitAccount );
entityManager.persist( creditAccount );
entityManager.persist( account );
entityManager.unwrap( Session.class ).doWork( connection -> {
try(Statement statement = connection.createStatement()) {
statement.executeUpdate(
"insert into Account (DTYPE, active, balance, interestRate, owner, id) " +
"values ('Other', true, 25, 0.5, 'Vlad', 4)"
);
}
} );
Map<Long, Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class )
.getResultList()
.stream()
.collect( Collectors.toMap( Account::getId, Function.identity()));
assertEquals(4, accounts.size());
assertEquals( DebitAccount.class, accounts.get( 1L ).getClass() );
assertEquals( CreditAccount.class, accounts.get( 2L ).getClass() );
assertEquals( Account.class, accounts.get( 3L ).getClass() );
assertEquals( OtherAccount.class, accounts.get( 4L ).getClass() );
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'Debit', 1)
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (1000, 1.9, 'John Doe', 5000, 'Credit', 2)
INSERT INTO Account (balance, interestRate, owner, id)
VALUES (1000, 1.9, 'John Doe', 3)
INSERT INTO Account (DTYPE, active, balance, interestRate, owner, id)
VALUES ('Other', true, 25, 0.5, 'Vlad', 4)
SELECT a.id as id2_0_,
a.balance as balance3_0_,
a.interestRate as interest4_0_,
a.owner as owner5_0_,
a.overdraftFee as overdraf6_0_,
a.creditLimit as creditLi7_0_,
a.active as active8_0_,
a.DTYPE as DTYPE1_0_
FROM Account a
As you can see, the Account entity row has a value of NULL in the DTYPE discriminator column,
while the OtherAccount entity was saved with a DTYPE column value of other which has not explicit mapping.
Joined table
Each subclass can also be mapped to its own table. This is also called table-per-subclass mapping strategy. An inherited state is retrieved by joining with the table of the superclass.
A discriminator column is not required for this mapping strategy. Each subclass must, however, declare a table column holding the object identifier.
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.JOINED)
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE Account (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE CreditAccount (
creditLimit NUMERIC(19, 2) ,
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE DebitAccount (
overdraftFee NUMERIC(19, 2) ,
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
ALTER TABLE CreditAccount
ADD CONSTRAINT FKihw8h3j1k0w31cnyu7jcl7n7n
FOREIGN KEY (id) REFERENCES Account
ALTER TABLE DebitAccount
ADD CONSTRAINT FKia914478noepymc468kiaivqm
FOREIGN KEY (id) REFERENCES Account
|
The primary key of this table is also a foreign key to the superclass table and described by the The table name still defaults to the non-qualified class name.
Also, if |
@PrimaryKeyJoinColumn@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.JOINED)
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
@PrimaryKeyJoinColumn(name = "account_id")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
@PrimaryKeyJoinColumn(name = "account_id")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE CreditAccount (
creditLimit NUMERIC(19, 2) ,
account_id BIGINT NOT NULL ,
PRIMARY KEY ( account_id )
)
CREATE TABLE DebitAccount (
overdraftFee NUMERIC(19, 2) ,
account_id BIGINT NOT NULL ,
PRIMARY KEY ( account_id )
)
ALTER TABLE CreditAccount
ADD CONSTRAINT FK8ulmk1wgs5x7igo370jt0q005
FOREIGN KEY (account_id) REFERENCES Account
ALTER TABLE DebitAccount
ADD CONSTRAINT FK7wjufa570onoidv4omkkru06j
FOREIGN KEY (account_id) REFERENCES Account
When using polymorphic queries, the base class table must be joined with all subclass tables to fetch every associated subclass instance.
List<Account> accounts = entityManager
.createQuery( "select a from Account a" )
.getResultList();
SELECT jointablet0_.id AS id1_0_ ,
jointablet0_.balance AS balance2_0_ ,
jointablet0_.interestRate AS interest3_0_ ,
jointablet0_.owner AS owner4_0_ ,
jointablet0_1_.overdraftFee AS overdraf1_2_ ,
jointablet0_2_.creditLimit AS creditLi1_1_ ,
CASE WHEN jointablet0_1_.id IS NOT NULL THEN 1
WHEN jointablet0_2_.id IS NOT NULL THEN 2
WHEN jointablet0_.id IS NOT NULL THEN 0
END AS clazz_
FROM Account jointablet0_
LEFT OUTER JOIN DebitAccount jointablet0_1_ ON jointablet0_.id = jointablet0_1_.id
LEFT OUTER JOIN CreditAccount jointablet0_2_ ON jointablet0_.id = jointablet0_2_.id
|
The joined table inheritance polymorphic queries can use several JOINS which might affect performance when fetching a large number of entities. |
Table per class
A third option is to map only the concrete classes of an inheritance hierarchy to tables. This is called the table-per-concrete-class strategy. Each table defines all persistent states of the class, including the inherited state.
In Hibernate, it is not necessary to explicitly map such inheritance hierarchies. You can map each class as a separate entity root. However, if you wish use polymorphic associations (e.g. an association to the superclass of your hierarchy), you need to use the union subclass mapping.
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public static class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
private BigDecimal interestRate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getInterestRate() {
return interestRate;
}
public void setInterestRate(BigDecimal interestRate) {
this.interestRate = interestRate;
}
}
@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {
private BigDecimal overdraftFee;
public BigDecimal getOverdraftFee() {
return overdraftFee;
}
public void setOverdraftFee(BigDecimal overdraftFee) {
this.overdraftFee = overdraftFee;
}
}
@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {
private BigDecimal creditLimit;
public BigDecimal getCreditLimit() {
return creditLimit;
}
public void setCreditLimit(BigDecimal creditLimit) {
this.creditLimit = creditLimit;
}
}
CREATE TABLE Account (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE CreditAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
creditLimit NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
CREATE TABLE DebitAccount (
id BIGINT NOT NULL ,
balance NUMERIC(19, 2) ,
interestRate NUMERIC(19, 2) ,
owner VARCHAR(255) ,
overdraftFee NUMERIC(19, 2) ,
PRIMARY KEY ( id )
)
When using polymorphic queries, a UNION is required to fetch the base class table along with all subclass tables as well.
List<Account> accounts = entityManager
.createQuery( "select a from Account a" )
.getResultList();
SELECT tablepercl0_.id AS id1_0_ ,
tablepercl0_.balance AS balance2_0_ ,
tablepercl0_.interestRate AS interest3_0_ ,
tablepercl0_.owner AS owner4_0_ ,
tablepercl0_.overdraftFee AS overdraf1_2_ ,
tablepercl0_.creditLimit AS creditLi1_1_ ,
tablepercl0_.clazz_ AS clazz_
FROM (
SELECT id ,
balance ,
interestRate ,
owner ,
CAST(NULL AS INT) AS overdraftFee ,
CAST(NULL AS INT) AS creditLimit ,
0 AS clazz_
FROM Account
UNION ALL
SELECT id ,
balance ,
interestRate ,
owner ,
overdraftFee ,
CAST(NULL AS INT) AS creditLimit ,
1 AS clazz_
FROM DebitAccount
UNION ALL
SELECT id ,
balance ,
interestRate ,
owner ,
CAST(NULL AS INT) AS overdraftFee ,
creditLimit ,
2 AS clazz_
FROM CreditAccount
) tablepercl0_
|
Polymorphic queries require multiple UNION queries, so be aware of the performance implications of a large class hierarchy. |
Implicit and explicit polymorphism
By default, when you query a base class entity, the polymorphic query will fetch all subclasses belonging to the base type.
However, you can even query interfaces or base classes that don’t belong to the JPA entity inheritance model.
For instance, considering the following DomainModelEntity interface:
public interface DomainModelEntity<ID> {
ID getId();
Integer getVersion();
}
If we have two entity mappings, a Book and a Blog,
and the Book entity is mapped with the
@Polymorphism annotation
and taking the PolymorphismType.EXPLICIT setting:
@Polymorphism entity mapping@Entity(name = "Event")
public static class Book implements DomainModelEntity<Long> {
@Id
private Long id;
@Version
private Integer version;
private String title;
private String author;
//Getter and setters omitted for brevity
}
@Entity(name = "Blog")
@Polymorphism(type = PolymorphismType.EXPLICIT)
public static class Blog implements DomainModelEntity<Long> {
@Id
private Long id;
@Version
private Integer version;
private String site;
//Getter and setters omitted for brevity
}
If we have the following entity objects in our system:
Book book = new Book();
book.setId( 1L );
book.setAuthor( "Vlad Mihalcea" );
book.setTitle( "High-Performance Java Persistence" );
entityManager.persist( book );
Blog blog = new Blog();
blog.setId( 1L );
blog.setSite( "vladmihalcea.com" );
entityManager.persist( blog );
We can now query against the DomainModelEntity interface,
and Hibernate is going to fetch only the entities that are either mapped with
@Polymorphism(type = PolymorphismType.IMPLICIT)
or they are not annotated at all with the @Polymorphism annotation (implying the IMPLICIT behavior):
List<DomainModelEntity> accounts = entityManager
.createQuery(
"select e " +
"from org.hibernate.userguide.inheritance.polymorphism.DomainModelEntity e" )
.getResultList();
assertEquals(1, accounts.size());
assertTrue( accounts.get( 0 ) instanceof Book );
Therefore, only the Book was fetched since the Blog entity was marked with the
@Polymorphism(type = PolymorphismType.EXPLICIT) annotation, which instructs Hibernate
to skip it when executing a polymorphic query against a non-mapped base class.
Immutability
Immutability can be specified for both entities and collections.
Entity immutability
If a specific entity is immutable, it is good practice to mark it with the @Immutable annotation.
@Entity(name = "Event")
@Immutable
public static class Event {
@Id
private Long id;
private Date createdOn;
private String message;
//Getters and setters are omitted for brevity
}
Internally, Hibernate is going to perform several optimizations, such as:
-
reducing memory footprint since there is no need to retain the dehydrated state for the dirty checking mechanism
-
speeding-up the Persistence Context flushing phase since immutable entities can skip the dirty checking process
Considering the following entity is persisted in the database:
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = new Event();
event.setId( 1L );
event.setCreatedOn( new Date( ) );
event.setMessage( "Hibernate User Guide rocks!" );
entityManager.persist( event );
} );
When loading the entity and trying to change its state,
Hibernate will skip any modification, therefore no SQL UPDATE statement is executed.
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = entityManager.find( Event.class, 1L );
log.info( "Change event message" );
event.setMessage( "Hibernate User Guide" );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
Event event = entityManager.find( Event.class, 1L );
assertEquals("Hibernate User Guide rocks!", event.getMessage());
} );
SELECT e.id AS id1_0_0_,
e.createdOn AS createdO2_0_0_,
e.message AS message3_0_0_
FROM event e
WHERE e.id = 1
-- Change event message
SELECT e.id AS id1_0_0_,
e.createdOn AS createdO2_0_0_,
e.message AS message3_0_0_
FROM event e
WHERE e.id = 1
Collection immutability
Just like entities, collections can also be marked with the @Immutable annotation.
Considering the following entity mappings:
@Entity(name = "Batch")
public static class Batch {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@Immutable
private List<Event> events = new ArrayList<>( );
//Getters and setters are omitted for brevity
}
@Entity(name = "Event")
@Immutable
public static class Event {
@Id
private Long id;
private Date createdOn;
private String message;
//Getters and setters are omitted for brevity
}
This time, not only the Event entity is immutable, but the Event collection stored by the Batch parent entity.
Once the immutable collection is created, it can never be modified.
doInJPA( this::entityManagerFactory, entityManager -> {
Batch batch = new Batch();
batch.setId( 1L );
batch.setName( "Change request" );
Event event1 = new Event();
event1.setId( 1L );
event1.setCreatedOn( new Date( ) );
event1.setMessage( "Update Hibernate User Guide" );
Event event2 = new Event();
event2.setId( 2L );
event2.setCreatedOn( new Date( ) );
event2.setMessage( "Update Hibernate Getting Started Guide" );
batch.getEvents().add( event1 );
batch.getEvents().add( event2 );
entityManager.persist( batch );
} );
The Batch entity is mutable. Only the events collection is immutable.
For instance, we can still modify the entity name:
doInJPA( this::entityManagerFactory, entityManager -> {
Batch batch = entityManager.find( Batch.class, 1L );
log.info( "Change batch name" );
batch.setName( "Proposed change request" );
} );
SELECT b.id AS id1_0_0_,
b.name AS name2_0_0_
FROM Batch b
WHERE b.id = 1
-- Change batch name
UPDATE batch
SET name = 'Proposed change request'
WHERE id = 1
However, when trying to modify the events collection:
try {
doInJPA( this::entityManagerFactory, entityManager -> {
Batch batch = entityManager.find( Batch.class, 1L );
batch.getEvents().clear();
} );
}
catch ( Exception e ) {
log.error( "Immutable collections cannot be modified" );
}
javax.persistence.RollbackException: Error while committing the transaction
Caused by: javax.persistence.PersistenceException: org.hibernate.HibernateException:
Caused by: org.hibernate.HibernateException: changed an immutable collection instance: [
org.hibernate.userguide.immutability.CollectionImmutabilityTest$Batch.events#1
]
|
While immutable entity changes are simply discarded, modifying an immutable collection end up in a |