I got some trouble loading a list of objects from my database using Hibernate and lazy=true mode. Hope that someone can help me out here.
I have a simple class here called UserAccount which looks like this:
public class UserAccount {
long id;
String username;
List<MailAccount> mailAccounts = new Vector<MailAccount>();
public UserAccount(){
super();
}
public long getId(){
return id;
}
public void setId(long id){
this.id = id;
}
public String getUsername(){
return username;
}
public void setUsername(String username){
this.username = username;
}
public List<MailAccount> getMailAccounts() {
if (mailAccounts == null) {
mailAccounts = new Vector<MailAccount>();
}
return mailAccounts;
}
public void setMailAccounts(List<MailAccount> mailAccounts) {
this.mailAccounts = mailAccounts;
}
}
I am mapping this class in Hibernate via the following mapping file:
<?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>
<class name="test.account.UserAccount" table="USERACCOUNT">
<id name="id" type="long" access="field">
<column name="USER_ACCOUNT_ID" />
<generator class="native" />
</id>
<property name="username" />
<bag name="mailAccounts" table="MAILACCOUNTS" lazy="true" inverse="true" cascade="all">
<key column="USER_ACCOUNT_ID"></key>
<one-to-many class="test.account.MailAccount" />
</bag>
</class>
</hibernate-mapping>
As you can see, lazy is set to "true" in the bag mapping element.
Saving the data to the database works fine:
Loading also works by calling loadUserAccount(String username)
(see code below):
public class HibernateController implements DatabaseController {
private Session session = null;
private final SessionFactory sessionFactory = buildSessionFactory();
public HibernateController() {
super();
}
private SessionFactory buildSessionFactory() {
try {
return new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public UserAccount loadUserAccount(String username) throws FailedDatabaseOperationException {
UserAccount account = null;
Session session = null;
Transaction transaction = null;
try {
session = getSession();
transaction = session.beginTransaction();
Query query = session.createQuery("FROM UserAccount WHERE username = :uname").setParameter("uname", username));
account = (UserAccount) query.uniqueResult();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
throw new FailedDatabaseOperationException(e);
} finally {
if (session.isOpen()) {
// session.close();
}
}
return account;
}
private Session getSession() {
if (session == null){
session = getSessionFactory().getCurrentSession();
}
return session;
}
}
The problem is just: When I access elements within the list "mailAccounts", I get the following exception:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: test.account.UserAccount.mailAccounts, no session or session was closed
I assume the reason for this exception is that the session got closed (don't know why and how) and thus Hibernate cannot load the list.
As you can see, I even removed the session.close()
call from the loadUserAccount()
method but the session still seems to be either get closed or replaced by another instance.
If I set lazy=false
, then everything works smoothly but this is not what I wanted because I need the feature of loading data "on demand" due to performance issues.
So, if I can't be sure that my session is still valid after the method loadUserAccount(String username)
terminated, what's the point of having that feature and how do I work around that?
Thanks for your help!
Ps: I am a Hibernate beginner so please excuse my noobishness.
Update: Here is my hibernate config.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">foo</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mytable</property>
<property name="hibernate.connection.username">user</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
<!-- Auto create tables -->
<!-- <property name="hbm2ddl.auto">create</property>-->
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- Mappings -->
<mapping resource="test/account/SmampiAccount.hbm.xml"/>
<mapping resource="test/account/MailAccount.hbm.xml"/>
</session-factory>
</hibernate-configuration>
Lazy Loading working or not has nothing to do with transaction boundaries. It only requires that the Session be open.
However, when the session is open depends on how you've actually set up the SessionFactory, which you did not tell us! There is config going on behind what SessionFactory.getCurrentSession()
actually does! If you're letting it go with the default version of ThreadLocalSessionContext
and not doing anything to manage the life cycle, it does indeed actually default to closing the session when you commit. (Hence the common conception that broadening transaction boundaries is the 'fix' for a lazy load exception.)
If you manage you own session life cycle with sessionFactory.openSession()
and session.close()
you will be able to lazy load fine within the session life cycle, outside transaction boundaries. Alternately you can provide a subclass of ThreadLocalSessionContext
that manages the session life-cycle with the boundaries you desire. There are also readily available alternatives such as the OpenSessionInView filter that can be used in web applications to bind the session life-cycle to the web request life cycle.
edit: You can also of course just initialize the list inside the transaction if that works for you. I just think that leads to really clunky APIs when you need either a new method signature of some kind of 'flag' parameter for each level of hydration of your entity. dao.getUser() dao.getUserWithMailAccounts() dao.getUserWIthMailAccountsAndHistoricalIds() and so on.
edit 2: You may find this helpful for different approaches to how long the session stays open/the relationship between session scope and transaction scope. (particularly the idea of session-per-request-with-detached-objects vs session-per-conversation.)
It depends on your requirements and architecture just how big a conversation actually is.
The reason you're getting the exception might be that the transaction you load the data in is closed (and the session with it), i.e. you're working outside the session. Lazy loading is especially useful when working with entities in one session (or across sessions when correctly employing a second level cache).
AFAIK you can tell Hibernate to automatically open a new session for lazy loading but I didn't use that for a while and thus I'd have to look up how that works again.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With