Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JPA and Hibernate proxy behavior

i tried to observe JPA2 / Hibernate4 proxy behavior below,

// Circular entity with lazy loading:

@Entity
public class Employee {

 @Id@Generated
 int id;
 String name;
 @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
 Employee boss;

 public String toString() {
  return id + "|" + name + "|" + boss;
 }

 //getters and setters ...

}

// Persist entities:

// Outer entity:
Employee employee = new Employee();
employee.setName("engineer");
// Inner entity:
Employee boss = new Employee();
boss.setName("manager");
employee.setBoss(boss);

entityTransaction.begin();
entityManager.persist(employee);
entityTransaction.commit();
System.out.println(employee);

// Output:

Hibernate: insert into Employee (id, boss_id, name) values (default, ?, ?)
Hibernate: insert into Employee (id, boss_id, name) values (default, ?, ?)

2|engineer|1|manager|null

// Load the outer entity:

String queryString = "select e from Employee e where e.id=" + employee.getId();
Query query = entityManager.createQuery(queryString);
Object loadedEmployee = query.getSingleResult();
System.out.println(loadedEmployee.getClass().getSimpleName());

// Output:

Hibernate: select employee0_.id as id2_, employee0_.boss_id as boss3_2_, employee0_.name as name2_ from Employee employee0_ where employee0_.id=2 limit ?

Employee

To my surprise the loaded outer entity above is still the plain one, but i'd expected it to be Hibernate proxy resulting from lazy loading. I could have missed something here, so how to get it right? A simple yet concrete example is much appreciated!

@EDIT

According to the answer from @kostja i adapted the code and debugged it in SE mode below, neither could LazyInitializationException be produced, nor was boss property proxied. Any further hints?

code window

debug window

@EDIT 2

Finally, i'd confirm that the answer from @kostja is undoubtly great.

I tested in EE mode, so the proxied boss property was observed below,

// LazyInitializationException thrown:

public Employee retrieve(int id) {
 Employee employee = entityManager.find(Employee.class, id);
 // access to the proxied boss property outside of persistence/transaction ctx
 Employee boss = employee.getBoss();
 System.out.println(boss instanceof HibernateProxy);
 System.out.println(boss.getClass().getSimpleName());
 return boss;
}

// Green light after put Spring Tx in place:

@Transactional
public Employee retrieve(int id) ...

// Output:

true
Employee_$$_javassist_0

Also, one can refer to 20.1.4. Initializing collections and proxies from Hibernate docs.

like image 829
sof Avatar asked Nov 14 '12 15:11

sof


1 Answers

This is the expected JPA behaviour. There is no reason for the entity from your query to be proxied - it is a regular result of a query. The boss property of this entity however should be a proxy. It will not tell if asked though - when you execute any operations on the lazily loaded property of a managed entity, it will trigger the fetch.

So you should access the boss property outside the transaction. If it has not been fetched, you will get a LazyInitializationException.

How you would go about it depends on the kind of the EntityManager and PersistenceContext.

  • works only since JPA 2.0 - call em.detach(loadedEmployee) and then access the boss property.

For JPA 1:

  • If you are in a Java EE environment, mark the method with @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) to suspend the transaction.

  • In an SE environment with user transactions, call transaction.commit() before accessing the boss property.

  • If using an EXTENDED PersistenceContext that will outlive the transaction, call em.clear().

EIDT: I suppose that the reason you are not getting the exception is that FetchType.LAZY is just a hint for the JPA provider, so there is no guarantee for the property to be loaded lazily. Contrary to that, FetchType.EAGER guarantees eager fetching. I suppose, your JPA provider chooses to load eagerly.

I have reproduced the example, though a bit differently and I am reproducibly getting the LazyInitializationException on the log statement. The test is an Arquillian test running on JBoss 7.1.1 with JPA 2.0 over Hibernate 4.0.1:

@RunWith(Arquillian.class)
public class CircularEmployeeTest {
    @Deployment
    public static Archive<?> createTestArchive() {
        return ShrinkWrap
                .create(WebArchive.class, "test.war")
                .addClasses(Employee.class, Resources.class)
                .addAsResource("META-INF/persistence.xml",
                        "META-INF/persistence.xml")
                .addAsResource("testSeeds/2CircularEmployees.sql", "import.sql")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @PersistenceContext
    EntityManager em;

    @Inject
    UserTransaction tx;

    @Inject
    Logger log;

    @Test
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public void testConfirmLazyLoading() throws Exception {
        String query = "SELECT e FROM Employee e WHERE e.id = 1";

        tx.begin();
        Employee employee = em.createQuery(query,
                Employee.class).getSingleResult();
        tx.commit();
        log.info("retrieving the boss: {}", employee.getBoss());
    }
}
like image 178
kostja Avatar answered Oct 13 '22 16:10

kostja