437 lines
20 KiB
Plaintext
437 lines
20 KiB
Plaintext
= 7.0 Migration Guide
|
|
:toc:
|
|
:toclevels: 4
|
|
:docsBase: https://docs.jboss.org/hibernate/orm
|
|
:versionDocBase: {docsBase}/7.0
|
|
:userGuideBase: {versionDocBase}/userguide/html_single/Hibernate_User_Guide.html
|
|
:javadocsBase: {versionDocBase}/javadocs
|
|
|
|
|
|
This guide discusses migration to Hibernate ORM version 7.0. For migration from
|
|
earlier versions, see any other pertinent migration guides as well.
|
|
|
|
[[jpa-32]]
|
|
== Jakarta Persistence 3.2
|
|
|
|
7.0 migrates to Jakarta Persistence 3.2 which is fairly disruptive, mainly around:
|
|
|
|
* type parameters
|
|
** Affects much of the Criteria API - especially roots, joins, paths
|
|
** Affects much of the Graph API -
|
|
*** org.hibernate.graph.Graph.addAttributeNode(java.lang.String) defines a return while
|
|
`jakarta.persistence.Graph.addAttributeNode(java.lang.String)` does not.
|
|
* new JPA features colliding with previous Hibernate extension features
|
|
** `Nulls` (JPA) v. `NullPrecedence` (Hibernate), including JPA's new `Order#getNullPrecedence()` returning `Nulls`
|
|
colliding with Hibernate's `SqmSortSpecification#getNullPrecedence` returning `NullPrecedence`. Hibernate's form
|
|
was renamed to `SqmSortSpecification#getHibernateNullPrecedence` to avoid the collision.
|
|
** `SchemaManager` is now also a JPA contract exposed as `EntityManagerFactory#getSchemaManager` which leads to type issues for
|
|
Hibernate's `SessionFactory#getSchemaManager`. Hibernate's `SchemaManager` now extends the new JPA `SchemaManager`.
|
|
But that is a bytecode incompatibility.
|
|
** JPA has added support in its Graph API for things Hibernate has supported for some time. Some of those are collisions
|
|
requiring changes to the Hibernate API.
|
|
** `Transaction#getTimeout`. JPA 3.2 adds `#getTimeout` but uses `Integer` whereas Hibernate has historically used `int`. Note that this raises the possibility of a `NullPointerException` during migration if, e.g., performing direct comparisons on the timeout value against an in (auto unboxing).
|
|
|
|
See this https://in.relation.to/2024/04/01/jakarta-persistence-3/[blog post] for a good discussion of the changes in Jakarta Persistence 3.2.
|
|
|
|
|
|
[[hibernate-models]]
|
|
== Hibernate Models
|
|
|
|
For many years Hibernate has used the Hibernate Commons Annotations (HCANN) library for handling various low-level tasks
|
|
related to understanding the structure of an application domain model, reading annotations and weaving in XML
|
|
mapping documents.
|
|
|
|
However, HCANN suffers from a number of limitations that continued to be problematic. And given
|
|
the use of HCANN across multiple projects, doing the needed refactoring was simply not possible.
|
|
|
|
The https://github.com/hibernate/hibernate-models[Hibernate Models] project was developed to be a better alternative
|
|
to HCANN. Hibernate Models is essentially an abstraction over reflection (`Type`, `Class`, `Member`, ...) and
|
|
annotations. Check out its project page for complete details.
|
|
|
|
7.0 uses Hibernate Models in place of HCANN.
|
|
|
|
NOTE: Currently, the `hibernate-envers` module still uses HCANN. That will change during continued 7.x development.
|
|
|
|
|
|
|
|
[[model-validation]]
|
|
== Domain Model Validations
|
|
|
|
7.0 adds many more checks about illegal use of annotations.
|
|
|
|
[[PersistentAttributeType]]
|
|
=== PersistentAttributeType
|
|
|
|
As of 7.0, Hibernate applies much better validation of an attribute specifying multiple PersistentAttributeTypes.
|
|
Jakarta Persistence 3.2 has clarified this in the specification. E.g., the following examples are all now illegal -
|
|
|
|
[source,java]
|
|
----
|
|
@Basic
|
|
@ManyToOne
|
|
private Employee manager;
|
|
----
|
|
|
|
or
|
|
|
|
[source,java]
|
|
----
|
|
@Lob
|
|
@ManyToOne
|
|
private Employee manager;
|
|
----
|
|
|
|
|
|
[[misplaced-annotations]]
|
|
=== Misplaced Annotations
|
|
|
|
7.0 does much more in-depth checking that annotations appear in the proper place. While previous versions
|
|
did not necessarily throw errors, in most cases these annotations were simply ignored. E.g.
|
|
|
|
|
|
[source,java]
|
|
----
|
|
@Entity
|
|
class Book {
|
|
// defines FIELD access-type
|
|
@Id
|
|
Integer id;
|
|
|
|
// previously ignored, this is an error now
|
|
@Column(name="category")
|
|
String getType() { ... }
|
|
}
|
|
----
|
|
|
|
[[id-generators]]
|
|
=== Identifier Generators
|
|
|
|
Starting in 7.0 it is no longer valid to combine `GenerationType#SEQUENCE` with anything other than
|
|
`@SequenceGenerator` nor `GenerationType#TABLE` with anything other than `@TableGenerator`. Previous
|
|
versions did not validate this particularly well.
|
|
|
|
|
|
[[java-beans]]
|
|
=== JavaBean Conventions
|
|
|
|
Previous versions allowed some questionable (at best) attribute naming patterns. These are no longer supported. E.g.
|
|
|
|
[source,java]
|
|
----
|
|
@Basic
|
|
String isDefault();
|
|
----
|
|
|
|
|
|
[[create-query]]
|
|
== Queries with implicit `select` list and no explicit result type
|
|
|
|
In previous versions, Hibernate allowed a query with no `select` list to be passed to the overload of `createQuery()` with no explicit result type parameter, for example:
|
|
|
|
[source,java]
|
|
List query =
|
|
session.createQuery("from X, Y")
|
|
.getResultList()
|
|
|
|
or:
|
|
|
|
[source,java]
|
|
List query =
|
|
session.createQuery("from X join y")
|
|
.getResultList()
|
|
|
|
The select list was inferred based on the `from` clause.
|
|
|
|
In Hibernate 6 we decided to deprecate this overload of `createQuery()`, since:
|
|
|
|
- it returns a raw type, resulting in compiler warnings in client code, and
|
|
- the second query is truly ambiguous, with no obviously intuitive interpretation.
|
|
|
|
As of Hibernate 7, the method is remains deprecated, and potentially-ambiguous queries _are no longer accepted_.
|
|
Migration paths include:
|
|
|
|
1. explicitly specify the `select` list,
|
|
2. add `X.class` or `Object[].class` as a second argument, to disambiguate the interpretation of the query, or
|
|
3. in the case where the query should return exactly one entity, explicitly assign the alias `this` to that entity.
|
|
|
|
For example, the queries above may be migrated via:
|
|
|
|
[source,java]
|
|
List<Object[]> result =
|
|
session.createQuery("from X, Y", Object[].class)
|
|
.getResultList()
|
|
|
|
or:
|
|
|
|
[source,java]
|
|
List<X> result =
|
|
session.createQuery("from X join y", X.class)
|
|
.getResultList()
|
|
|
|
|
|
[[proxy-annotation]]
|
|
== Replace `@Proxy`
|
|
|
|
Applications will need to replace usages of the removed `@Proxy` annotation.
|
|
|
|
`@Proxy#proxyClass` has no direct replacement, but was also never needed/useful.
|
|
|
|
Here we focus on `@Proxy#laxy` attribute which, again, was hardly ever useful.
|
|
By default (true), Hibernate would proxy an entity when possible and when asked for.
|
|
"Asked for" includes calls to `Session#getReference` and lazy associations.
|
|
All such cases though are already controllable by the application.
|
|
|
|
* Instead of `Session#getReference`, use `Session#find`
|
|
* Use eager associations, using
|
|
** `FetchType.EAGER` (the default for to-one associations anyway), possibly combined with `@Fetch`
|
|
** `EntityGraph`
|
|
** `@FetchProfiles`
|
|
|
|
The effect can also often be mitigated using Hibernate's bytecode-based laziness (possibly combined with `@ConcreteProxy`).
|
|
|
|
|
|
[[flush-persist]]
|
|
== Session flush and persist
|
|
|
|
The removal of `CascadeType.SAVE_UPDATE` slightly changes the persist and flush behaviour to conform with Jakarta Persistence.
|
|
|
|
Persisting a transient entity or flushing a manged entity with an associated detached entity having the association annotated with `cascade = CascadeType.ALL` or `cascade = CascadeType.PERSIST` throws now an `jakarta.persistence.EntityExistsException` if the detached entity has not been re-associated with the Session.
|
|
|
|
To re-associate the detached entity with the Session the `Session#merge` method can be used.
|
|
|
|
Consider the following model
|
|
|
|
[source,java]
|
|
----
|
|
@Entity
|
|
class Parent {
|
|
...
|
|
|
|
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent", orphanRemoval = true)
|
|
@LazyCollection(value = LazyCollectionOption.EXTRA)
|
|
private Set<Child> children = new HashSet<>();
|
|
|
|
public void addChild(Child child) {
|
|
children.add( child );
|
|
child.setParent( this );
|
|
}
|
|
}
|
|
|
|
@Entity
|
|
class Child {
|
|
...
|
|
|
|
@ManyToOne
|
|
private Parent parent;
|
|
}
|
|
----
|
|
|
|
Assuming we have `c1` as a detached `Child`, the following code will now result in `jakarta.persistence.EntityExistsException` being thrown at flush time:
|
|
|
|
[source,java]
|
|
----
|
|
Parent parent = session.get( Parent.class, parentId );
|
|
parent.addChild( c1 );
|
|
----
|
|
|
|
Instead, `c1` must first be re-associated with the Session using merge:
|
|
|
|
|
|
[source,java]
|
|
----
|
|
Parent parent = session.get( Parent.class, parentId );
|
|
Child merged = session.merge( c1 );
|
|
parent.addChild( merged );
|
|
----
|
|
|
|
|
|
[[refresh-lock-deteached]]
|
|
== Refreshing/locking detached entities
|
|
|
|
Traditionally, Hibernate allowed detached entities to be refreshed. However, Jakarta Persistence prohibits this practice and specifies that an `IllegalArgumentException` should be thrown instead. Hibernate now fully aligns with the JPA specification in this regard.
|
|
|
|
Along the same line of thought, also acquiring a lock on a detached entity is no longer allowed.
|
|
|
|
To this effect the `hibernate.allow_refresh_detached_entity`, which allowed Hibernate's legacy refresh behaviour to be invoked, has been removed.
|
|
|
|
|
|
[[auto-cascade-persist]]
|
|
== Cascading persistence for `@Id` and `@MapsId` fields
|
|
|
|
Previously Hibernate automatically enabled `cascade=PERSIST` for association fields annotated `@Id` or `@MapsId`.
|
|
This was undocumented and unexpected behavior, and arguably against the intent of the Persistence specification.
|
|
|
|
Existing code which relies on this behavior should be modified by addition of explicit `cascade=PERSIST` to the association field.
|
|
|
|
|
|
[[enum-checks]]
|
|
== Enums and Check Constraints
|
|
|
|
Hibernate previously added support for generating check constraints for enums mapped using `@Enumerated`
|
|
as part of schema generation. 7.0 adds the same capability for enums mapped using an `AttributeConverter`,
|
|
by asking the converter to convert all the enum constants on start up.
|
|
|
|
[[datetime-native]]
|
|
== Date and time types returned by native queries
|
|
|
|
In the absence of a `@SqlResultSetMapping`, previous versions of Hibernate used `java.sql` types (`Date`, `Time`, `Timestamp`) to represent date/time types returned by a native query.
|
|
In 7.0, such queries return types defined by `java.time` (`LocalDate`, `LocalTime`, `LocalDateTime`) by default.
|
|
The previous behavior may be recovered by setting `hibernate.query.native.prefer_jdbc_datetime_types` to `true`.
|
|
|
|
|
|
[[ddl-implicit-datatype-timestamp]]
|
|
== Default precision for `timestamp` on some databases
|
|
|
|
The default precision for Oracle timestamps was changed to 9 i.e. nanosecond precision.
|
|
The default precision for SQL Server timestamps was changed to 7 i.e. 100 nanosecond precision.
|
|
|
|
Note that these changes only affect DDL generation.
|
|
|
|
[[array-mapping-changes-on-db2-sap-hana-and-sybase-ase]]
|
|
== Array mapping changes on DB2, SAP HANA and Sybase ASE
|
|
|
|
On DB2, SAP HANA and Sybase ASE, basic arrays now map to the `SqlTypes.XML_ARRAY` type code,
|
|
whereas previously, the dialect mapped arrays to `SqlTypes.VARBINARY`.
|
|
The `SqlTypes.XML_ARRAY` type uses the `xml` DDL type which enables using arrays in other features through the various XML functions.
|
|
|
|
The migration requires to read data and re-save it. Note that XML support on Sybase ASE is not enabled by default
|
|
and requires to run `sp_configure 'enable xml', 1`.
|
|
|
|
To retain backwards compatibility, configure the setting `hibernate.type.preferred_array_jdbc_type` to `VARBINARY`.
|
|
|
|
[[array-mapping-changes-on-mysql-mariadb]]
|
|
== Array mapping changes on MySQL/MariaDB
|
|
|
|
On MySQL and MariaDB, basic arrays now map to the `SqlTypes.JSON_ARRAY` type code,
|
|
whereas previously, the dialect mapped arrays to `SqlTypes.VARBINARY`.
|
|
The `SqlTypes.JSON_ARRAY` type uses the `json` DDL type which enables using arrays in other features through the various JSON functions.
|
|
|
|
The migration requires to read data and re-save it.
|
|
|
|
To retain backwards compatibility, configure the setting `hibernate.type.preferred_array_jdbc_type` to `VARBINARY`.
|
|
|
|
[[sf-name]]
|
|
== SessionFactory Name (and JNDI)
|
|
|
|
Hibernate defines `SessionFactory#getName` (specified via `cfg.xml` or `hibernate.session_factory_name`) which is used to
|
|
help with (de)serializing a `SessionFactory`. It is also, unless `hibernate.session_factory_name_is_jndi` is set to `false`,
|
|
used in biding the `SessionFactory` into JNDI.
|
|
|
|
This `SessionFactory#getName` method pre-dates Jakarta Persistence (and JPA). It now implements `EntityManagerFactory#getName`
|
|
inherited from Jakarta Persistence, which states that this name should come from the persistence-unit name.
|
|
To align with Jakarta Persistence (the 3.2 TCK tests this), Hibernate now considers the persistence-unit name if no
|
|
`hibernate.session_factory_name` is specified.
|
|
|
|
However, because `hibernate.session_factory_name` is also a trigger to attempt to bind the SessionFactory into JNDI,
|
|
this change to consider persistence-unit name, means that each `SessionFactory` created through Jakarta Persistence now
|
|
have a name and Hibernate attempted to bind these to JNDI.
|
|
|
|
To work around this we have introduced a new `hibernate.session_factory_jndi_name` setting that can be used to explicitly
|
|
specify a name for JNDI binding. The new behavior is as follows (assuming `hibernate.session_factory_name_is_jndi` is not explicitly configured):
|
|
|
|
* If `hibernate.session_factory_jndi_name` is specified, the name is used to bind into JNDI
|
|
* If `hibernate.session_factory_name` is specified, the name is used to bind into JNDI
|
|
|
|
Hibernate can use the persistence-unit name for binding into JNDI as well, but `hibernate.session_factory_name_is_jndi`
|
|
must be explicitly set to true.
|
|
|
|
[[configurable-generators]]
|
|
== Configurable generators
|
|
|
|
The signature of the `Configurable#configure` method changed from accepting just a `ServiceRegistry` instance to the new `GeneratorCreationContext` interface, which exposes a lot more useful information when configuring the generator itself. The old signature has been deprecated for removal, so you should migrate any custom `Configurable` generator implementation to the new one.
|
|
|
|
[[stateless-session-jdbc-batching]]
|
|
== JDBC batching with `StatelessSession`
|
|
|
|
Automatic JDBC batching has the side effect of delaying the execution of the batched operation, and this undermines the synchronous nature of operations performed through a stateless session.
|
|
In Hibernate 7, the configuration property `hibernate.jdbc.batch_size` now has no effect on a stateless session.
|
|
Automatic batching may be enabled by explicitly calling `setJdbcBatchSize()`.
|
|
However, the preferred approach is to explicitly batch operations via `insertMultiple()`, `updateMultiple()`, or `deleteMultiple()`.
|
|
|
|
[[hbm-transform]]
|
|
== hbm.xml Transformation
|
|
|
|
Hibernate's legacy `hbm.xml` mapping schema has been deprecated for quite some time, replaced by a new `mapping.xml`
|
|
schema. In 7.0, this `mapping.xml` is stabilized and we now offer a transformation of `hbm.xml` files into `mapping.xml` files.
|
|
|
|
This tool is available as both -
|
|
|
|
* build-time transformation (currently only offered as a Gradle plugin)
|
|
* run-time transformation, using `hibernate.transform_hbm_xml.enabled=true`
|
|
|
|
Build-time transformation is preferred.
|
|
|
|
[NOTE]
|
|
====
|
|
Initial versions of the transformation processed one file at a time.
|
|
This is now done across the entire set of `hbm.xml` files at once.
|
|
While most users will never see this change, it might impact integrations which tie-in to XML processing.
|
|
====
|
|
|
|
[[mysql-varchar]]
|
|
== Default DDL type for `char` and `Character` on MySQL
|
|
|
|
Previously, `char` and `Character` fields were, by default, mapped to `char(1)` columns by the schema export tool.
|
|
However, MySQL treats a `char(1)` containing a single space as an empty string, resulting in broken behavior for some HQL and SQL functions.
|
|
Now, `varchar(1)` is used by default.
|
|
|
|
[[cleanup]]
|
|
== Cleanup
|
|
|
|
* Annotations
|
|
** Removed `@Persister`
|
|
** Removed `@Proxy` - see <<proxy-annotation>>
|
|
** Removed `@SelectBeforeUpdate`
|
|
** Removed `@DynamicInsert#value` and `@DynamicUpdate#value`
|
|
** Removed `@Loader`
|
|
** Removed `@Table`
|
|
** Removed `@Where` and `@WhereJoinTable`
|
|
** Removed `@ForeignKey`
|
|
** Removed `@Index`
|
|
** Removed `@IndexColumn`
|
|
** Removed `@GeneratorType` (and `GenerationTime`, etc)
|
|
** Removed `@LazyToOne`
|
|
** Removed `@LazyCollection`
|
|
** Removed `@IndexColumn`
|
|
** Replaced uses of `CacheModeType` with `CacheMode`
|
|
** Removed `@TestForIssue` (for testing purposes) -> use `org.hibernate.testing.orm.junit.JiraKey` and `org.hibernate.testing.orm.junit.JiraKeyGroup`
|
|
|
|
* Classes/interfaces
|
|
** Removed `SqmQualifiedJoin` (all joins are qualified)
|
|
** Removed `AdditionalJaxbMappingProducer` -> `AdditionalMappingContributor`
|
|
** Removed `MetadataContributor` -> `AdditionalMappingContributor`
|
|
** Removed `EmptyInterceptor` -> implement `org.hibernate.Interceptor` directly
|
|
|
|
* Behavior
|
|
** Removed `org.hibernate.Session#save` in favor of `org.hibernate.Session#persist`
|
|
** Removed `org.hibernate.Session#saveOrUpdate` in favor `#persist` if the entity is transient or `#merge` if the entity is detached.
|
|
** Removed `org.hibernate.Session#update` in favor of `org.hibernate.Session.merge`
|
|
** Removed `org.hibernate.annotations.CascadeType.SAVE_UPDATE` in favor of `org.hibernate.annotations.CascadeType.PERSIST` + `org.hibernate.annotations.CascadeType.MERGE`
|
|
** Removed `org.hibernate.Session#delete` in favor of `org.hibernate.Session#remove`
|
|
** Removed `org.hibernate.annotations.CascadeType.DELETE` in favor of `org.hibernate.annotations.CascadeType#REMOVE`
|
|
** Removed `org.hibernate.Session#refresh(String entityName, Object object)` in favor of `org.hibernate.Session#refresh(Object object)`
|
|
** Removed `org.hibernate.Session#refresh(String entityName, Object object, LockOptions lockOptions)` in favor of `org.hibernate.Session#refresh(Object object, LockOptions lockOptions)`
|
|
** Removed `org.hibernate.integrator.spi.Integrator#integrate(Metadata,SessionFactoryImplementor,SessionFactoryServiceRegistry)` in favor of `org.hibernate.integrator.spi.Integrator#integrate(Metadata,BootstrapContext,SessionFactoryImplementor)`
|
|
** Removed `org.hibernate.Interceptor#onLoad(Object, Serializable, Object[] , String[] , Type[] )` in favour of `org.hibernate.Interceptor#onLoad(Object, Object, Object[], String[], Type[] )`
|
|
** Removed `org.hibernate.Interceptor#onFlushDirty(Object, Serializable, Object[] , Object[], String[] , Type[] )` in favour of `org.hibernate.Interceptor#onLoad(Object, Object, Object[], Object[], String[] , Type[] )`
|
|
** Removed `org.hibernate.Interceptor#onSave(Object, Serializable, Object[], String[], Type[])` in favour of `org.hibernate.Interceptor#onSave(Object, Object, Object[], String[], Type[])`
|
|
** Removed `org.hibernate.Interceptor#onDelete(Object, Serializable, Object[], String[], Type[])` in favour of `org.hibernate.Interceptor#onDelete(Object, Serializable, Object[], String[], Type[])`
|
|
** Removed `org.hibernate.Interceptor#onCollectionRecreate(Object, Serializable)` in favour of `org.hibernate.Interceptor#onCollectionRecreate(Object, Object)`
|
|
** Removed `org.hibernate.Interceptor#onCollectionRemove(Object, Serializable)` in favour of `org.hibernate.Interceptor#onCollectionRemove(Object, Object)`
|
|
** Removed `org.hibernate.Interceptor#onCollectionUpdate(Object, Serializable)` in favour of `org.hibernate.Interceptor#onCollectionUpdate(Object, Object)`
|
|
** Removed `org.hibernate.Interceptor#findDirty(Object, Serializable, Object[], Object[], String[], Type[])` in favour of `org.hibernate.Interceptor#findDirty(Object, Object, Object[], Object[], String[], Type[])`
|
|
** Removed `org.hibernate.Interceptor#getEntity(String, Serializable)` in favour of `org.hibernate.Interceptor#getEntity(String, Serializable)`
|
|
** Removed `org.hibernate.metamodel.spi.MetamodelImplementor` in favor of `org.hibernate.metamodela.MappingMetmodel` or `org.hibernate.metamodel.model.domain.JpaMetamodel`
|
|
** Removed `org.hibernate.Metamodel` in favor of `org.hibernate.metamodel.model.domain.JpaMetamodel`
|
|
|
|
* Settings
|
|
** Removed `hibernate.mapping.precedence` and friends
|
|
** Removed `hibernate.allow_refresh_detached_entity`
|
|
|
|
|
|
|
|
[[todo]]
|
|
== Todos (dev)
|
|
|
|
* Look for `todo (jpa 3.2)` comments
|
|
* Look for `todo (7.0)` comments |