Creation date: 2007-04-20T01:09:40
Dialogue style intro
Dev: We keep patching our Hibernate XML mappings and a new feature lands every sprint. I want to move to the Java Persistence API and keep Hibernate as the provider.
Lead: Why touch what works
Dev: The Java Persistence API gives us a common contract. We still run Hibernate under the hood, but the app stops depending on Hibernate Session and its custom features. We get cleaner annotations, a standard query language, and a shot at running on GlassFish or JBoss without rewiring everything.
Lead: Sounds like a rewrite.
Dev: Not a rewrite. A guided swap. Start with persistence.xml, move entity mappings to annotations, wrap code that calls Session, and adopt EntityManager. We test on a feature branch and merge when green.
Lead: Fine. Make a plan and show the numbers.
Evidence section
We live in a world where Java EE 5 ships with the Java Persistence API as a first class citizen. TopLink Essentials is the reference, OpenJPA is gaining steam, and Hibernate 3.2 already speaks Java Persistence with hibernate entitymanager. If you run on Spring 2 you get native support for JpaTransactionManager and easy wiring of the EntityManagerFactory. On the app server side, GlassFish has it baked in, and JBoss plays nice with it. That is the current baseline.
From a code health point of view the move pays off right away. Annotations beat XML for the common cases. Your entity declares itself with @Entity, fields document their mapping inline, and you can keep the schema naming close to the code. For teams that still like XML, Java Persistence allows orm.xml overrides, but the sweet spot is annotations plus small overrides where needed.
On queries, JPQL reads very close to HQL. Many HQL statements move over with small edits. You get a standard API for named queries and pagination with setFirstResult and setMaxResults. People who run on multiple app servers get a solid benefit from standard transactions and standard bootstrapping. In plain Java SE you create an EntityManagerFactory from persistence.xml. In Java EE you inject it. In Spring you wire it in a few lines. Same pattern, fewer surprises.
Portability gets better too. If in six months you want to test with TopLink Essentials or OpenJPA, you can. Will everything work on day one No. But the big pieces will. At that point your provider specific bits are in one place rather than spread across the codebase.
Performance is fine. Hibernate as provider still uses the same core engine and the same second level cache. Both @BatchSize and @Cache are available through annotations in the Hibernate namespace when you need them, but you can keep most code on the Java Persistence side. Lazy loading and dirty checking behave the same way. The buffer that the Java Persistence API adds is thin.
Risk comes from the change itself. You touch every entity and every repository. Tests protect you here. You also need to keep an eye on orphan delete, cascade rules, and bidirectional relationships. None of this is scary, but it needs a list and a checklist.
Implementation notes
Here is a path that worked for us with Hibernate as provider and Spring.
Step 1. Add Java Persistence jars
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.2.1.ga</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.2.1.ga</version> </dependency>
If you run on an app server with Java Persistence built in, scope these as provided. In plain Java SE add them to the classpath.
Step 2. Create persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="mainPU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>com.example.domain.Customer</class>
<class>com.example.domain.Order</class>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.EhCacheRegionFactory"/>
</properties>
</persistence-unit>
</persistence>Place this file under META-INF. In Java EE the container discovers it. In Spring or plain Java SE you create the factory with Persistence.createEntityManagerFactory.
Step 3. Move mappings to annotations
// Before XML snippet for reference
// <class name="com.example.domain.Customer" table="customer">
// <id name="id" column="id"><generator class="identity"/></id>
// <property name="email" column="email" not-null="true" unique="true"/>
// <set name="orders" inverse="true" lazy="true">
// <key column="customer_id"/>
// <one-to-many class="com.example.domain.Order"/>
// </set>
// </class>
package com.example.domain;
import javax.persistence.*;
import java.util.*;
@Entity
@Table(name = "customer")
@NamedQueries({
@NamedQuery(name = "Customer.byEmail", query = "select c from Customer c where c.email = :email")
})
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Order> orders = new HashSet<>();
// getters and setters
}package com.example.domain;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(name = "total", nullable = false)
private BigDecimal total;
@Column(name = "created_on", nullable = false)
private LocalDate createdOn = LocalDate.now();
// getters and setters
}Notice the move from set mapping in XML to @OneToMany with mappedBy. Orphan removal replaces special delete orphans settings. You still get lazy loading. Keep equals and hashCode simple. Use the primary key or a stable business key to avoid surprises in sets.
Step 4. Swap Session for EntityManager
// Before
public class CustomerRepository {
private final SessionFactory sessionFactory;
public CustomerRepository(SessionFactory sf) { this.sessionFactory = sf; }
public Customer findByEmail(String email) {
Session s = sessionFactory.getCurrentSession();
return (Customer) s.createQuery("from Customer c where c.email = :email")
.setString("email", email)
.uniqueResult();
}
}// After
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
public class CustomerRepository {
@PersistenceContext
private EntityManager em;
public Customer findByEmail(String email) {
return (Customer) em.createNamedQuery("Customer.byEmail")
.setParameter("email", email)
.getSingleResult();
}
public void save(Customer c) {
if (c.getId() == null) em.persist(c);
else em.merge(c);
}
}In Spring, wire it like this:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitName" value="mainPU"/> </bean> <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="emf"/> </bean> <tx:annotation-driven transaction-manager="txManager"/>
After that, you can use @Transactional on services. For Java EE, inject with @PersistenceContext and rely on container transactions.
Step 5. Review queries
Most HQL queries move to JPQL with tiny edits. Remove the class prefix in select new if you used the package omission trick. JPQL does not support some Hibernate only functions. If you rely on them, keep a small adapter layer where you call the provider API. Keep provider touches in one package and your code stays clean.
Step 6. Tests
Boot a tiny EntityManagerFactory against HSQLDB for fast tests. Verify lazy loading boundaries, cascade, and orphan removal. Keep a few tests against your real database to catch dialect quirks. The provider is still Hibernate so the dialect is the same. You are just driving it with the Java Persistence API.
Risks
Lazy loading outside transactions. If your web layer touches a lazy association after the transaction closes, you will still get lazy exceptions. With the Java Persistence API the fix is the same. Keep lazy reads within a transaction or fetch what you need with a join fetch query. Avoid the open session in view trick unless you accept the tradeoffs.
Cascade and orphan rules. Moving from XML to annotations can change defaults that you took for granted. Review each association. In many cases you want cascade = CascadeType.ALL and orphanRemoval = true on the parent side of a child collection. Write tests where you remove a child from the collection and flush. Confirm the row is deleted.
Equals and hashCode. Entities inside sets are sensitive to equals and hashCode. If you used natural keys, keep them stable across a transaction. If not, base equals and hashCode on the id only after it is assigned and fall back to object identity before that. Never mix both in the same collection.
Flush mode and writes. The default flush mode in the Java Persistence API is similar to Hibernate. Before query and commit. If you were calling Session.flush at custom points, inspect those flows and either keep explicit em.flush or adjust your transaction boundaries.
Provider features. A few Hibernate goodies do not exist in standard form. Example: @BatchSize or @Formula. You can still use them through the Hibernate annotations, but they reduce portability. Keep those in a small number of entities and document them with a comment. That way you know what to revisit if you ever test with another provider.
DDL generation. The Java Persistence API has hbm2ddl.auto through the Hibernate properties but it is better to keep DDL scripts under version control. Use validate in prod and create or update in local dev only. This avoids surprises on release day.
Tooling. IDE support is catching up. NetBeans has solid wizards for Java Persistence. Eclipse Dali is coming along and already useful for annotations. IntelliJ can handle annotations and JPQL editing but the feedback is not perfect yet. Nothing blocking here, just keep an eye on it.
Graceful exit
You do not need a big bang migration. Pick a feature slice and move that vertical to the Java Persistence API while keeping the rest on Hibernate core. Both can live side by side for a while. Here is the trick. Use one data access style per service layer class. That keeps transactions clear and surprises away. After two or three slices, most of your entity set will be on annotations and your queries will be JPQL. The last bits are usually custom UserTypes or formula fields. Leave those for last.
When you are ready to flip the default, point all new repositories at EntityManager, keep a thin shim to wrap the few places still tied to Session, and move them when you touch the code next time. That gives you a soft landing and no long freeze on new features.
If you want a safety rope, hide the persistence layer behind an interface and provide two tiny adapters. One talks Java Persistence and one talks Hibernate Session. Run your suite with both on the branch. Delete the old adapter when done. Clean and boring.
The pitch is simple. The Java Persistence API is the standard. Hibernate as provider keeps the parts we like. Our code becomes cleaner, our queries read the same, and we gain options on where to run. It is a change with a clear path, not a leap of faith. If you start today, your next feature ships on the new stack, and you will not want to go back.
Drop me a line if you want the sample project. It boots with HSQLDB, runs a few queries, and shows how to write a small repository with EntityManager, persistence.xml, and a couple of @NamedQuery examples. Copy, tweak, repeat.