views:

1605

answers:

5

In Hibernate we have two classes with the following classes with JPA mapping:

package com.example.hibernate

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Foo {
  private long id;
  private Bar bar;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  @ManyToOne(fetch = FetchType.LAZY)
  public Bar getBar() {
    return bar;
  }

  public void setBar(Bar bar) {
    this.bar = bar;
  }
}

package com.example.hibernate

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;


public class Bar {
  private long id;
  private String title;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  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;
  } 
}

Now when we load from the database an object from class Foo using session get e.g:

Foo foo = (Foo)session.get(Foo.class, 1 /* or some other id that exists in the DB*/); the Bar member of foo is a proxy object (in our case javassist proxy but it can be cglib one depending on the bytecode provider you use), that is not initialized. If you then use session.get to fetch the Bar object that is the member of the Foo class just loaded (we are in the same session), Hibernate does not issue another DB query and fetches the object from the session (first level) cache. The problem is this is a proxy to Bar class which is not initialized and trying to call this object getId() will return 0, and getTitle() will return null. Our current solution is pretty ugly and checks if the object returned from get is a proxy here is the code (form a generic DAO implementation):

@SuppressWarnings("unchecked")
@Override
@Transactional(readOnly = true)
public <T extends IEntity> T get(Class<T> clazz, Serializable primaryKey) throws DataAccessException {
  T entity = (T) currentSession().get(clazz, primaryKey);
  if (entity != null) {
    if (LOG.isWarnEnabled()) {
      LOG.warn("Object not found for class " + clazz.getName() + " with primary key " + primaryKey);
    }
  } else if (entity instanceof HibernateProxy){ // TODO: force initialization due to Hibernate bug
    HibernateProxy proxy = (HibernateProxy)entity;
    if (!Hibernate.isInitialized(proxy)) {
      Hibernate.initialize(proxy);
    }
    entity = (T)proxy.getHibernateLazyInitializer().getImplementation();
  }
  return entity;
}

Is there a better way to do this, couldn't find a solution in the Hibernate forum, and didn't find the issue in Hibernate's JIRA.

Note: we cannot just use foo.getBar() (which will initialize the proxy properly) to get the Bar class object, because the session.get operation to fetch the Bar object does not know (or care for that matter) that the Bar class is also a lazy member of a Foo object that was just fetched.

A: 

Not really seen this problem, although we do get intermittent Lazy Load errors - so perhaps we have the same problem, anyway, is it an option to use a different session for the loading of the Bar object - that should load it from scratch, I would expect...

Chris Kimpton
Loading in another session is not an option: the real problem is more involved but the short answer is that we can't.
talg
A: 

I am unable to reproduce the behaviour you are seeing. Here is my code:

@Entity
public class Foo {
    private Long id; private String name; private Bar bar;

    public Foo() { }
    public Foo(String name) { this.name = name; }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    @Basic
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @ManyToOne(fetch = FetchType.LAZY)
    public Bar getBar() { return bar; }
    public void setBar(Bar bar) { this.bar = bar; }
}

@Entity
public class Bar {
    private Long id; private String name;

    public Bar() { }
    public Bar(String name) { this.name = name; }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    @Basic
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

    public void testGets() {
     SessionFactory sf = new AnnotationConfiguration()
      .addPackage("hibtest")
                .addAnnotatedClass(Foo.class)
                .addAnnotatedClass(Bar.class)
      .configure().buildSessionFactory();
     Session session = null;
     Transaction txn = null;

     // Create needed data
     try {
      session = sf.openSession();
      txn = session.beginTransaction();

      // Create a Bar
      Bar bar = new Bar("Test Bar");
      session.save(bar);

      // Create a Foo
      Foo foo = new Foo("Test Foo");
      session.save(foo);

      foo.setBar(bar);

      txn.commit();
     } catch (HibernateException ex) {
      if (txn != null) txn.rollback();
      throw ex;
     } finally {
      if (session != null) session.close();
     }

     // Try the fetch
     try {
      session = sf.openSession();
      Foo foo = (Foo) session.get(Foo.class, 1L);
      Bar bar = (Bar) session.get(Bar.class, 1L);
      System.out.println(bar.getName());
     } finally {
      if (session != null) session.close();
     }
    }

And it all works fine, as one would expect.

binil
A: 

Do you actually need to do lazy loading?
Could you not set FetchType to EAGER instead and have it always loaded (properly) using a join?

Darren Greaves
I do need to do lazy loading since this property is not always needed (list view of Foo objects for example)
talg
I tend not to use lazy loading so for situations where sometimes you need the additional properties and sometimes you don't I tend to write two different methods - one that does the join and one that doesn't.It then gives you the opportunity to tune each query to do exactly what you want.
Darren Greaves
+1  A: 

I had a similar problem:

  • I did Session.save(nastyItem) to save an object into the Session. However, I did not fill in the property buyer which is mapped as update="false" insert="false" (this happens a lot when you have a composed primary key, then you map the many-to-one's as insert="false" update="false")
  • I a query to load a list of items, and the item which I just saved, happens to be part of the result set
  • now what goes wrong? Hibernate sees that the item was already in the cache, and Hibernate does not replace (probably not to break my earlier reference nastyItem) it with the newly loaded value, but uses MY nastyItem I have put into the Session cache myself. Even worse, now the lazy loading of the buyer is broken: it contains null.

To avoid these Session issues, I always do a flush and a clear after a save, merge, update or delete. Having to solve these nasty problems takes too much of my time :-(

A: 

You are doing something wrong. I did not test your code, but you should never need to force the initialization of proxies, the property accessors do that for you. If you are using Hibernate explicitly, never mind using JPA, since you already have lost portability.

Hibernate should detect automatically whenever it needs to fetch or write to db. If you issue a getProperty() from a proxy, hibernate or any other jpa provider should fetch the correspondent row from the db.

The only situation I'm not sure hibernate is clever enough is if you issue a save() and then issue a get() with the id of the saved object, there might be a problem if the save() didn't flush the object to db.

Miguel Ping