开发者

Architecture to avoid Hibernate LazyInitializationExceptions

I am in the beginning of my project. So I am trying to design an architecture which avoid Hibernate LazyInitializationExceptions. So far my applicationContext.xml has:

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation">
        <value>/WEB-INF/hibernate.cfg.xml</value>
    </property>
    <property name="configurationClass">
        <value>org.hibernate.cfg.AnnotationConfiguration</value>
    </property>        
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${hibernate.dialect}</prop>        
            <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>     
        </props>
    </property>
    <property name="eventListeners">
        <map>
            <entry key="merge">
                <bean class="org.springframework.orm.hibernate3.support.IdTransferringMergeEventListener"/>
            </entry>
        </map>
    </property>
</bean>

<bean id="dao" class="info.ems.hibernate.HibernateEMSDao" init-method="createSchema">
    <property name="hibernateTemplate">
        <bean class="org.springframework.orm.hibernate3.HibernateTemplate">
            <property name="sessionFactory" ref="sessionFactory"/>
            <property name="flushMode">
                <bean id="org.springframework.orm.hibernate3.HibernateAccessor.FLUSH_COMMIT" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>                    
            </property>
        </bean>
    </property>        
    <property name="schemaHelper">
        <bean class="info.ems.hibernate.SchemaHelper">                                
            <property name="driverClassName" value="${database.driver}"/>
            <property name="url" value="${database.url}"/>
            <property name="username" value="${database.username}"/>
            <property name="password" value="${database.password}"/>
            <property name="hibernateDialect" value="${hibernate.dialect}"/>   
            <property name="dataSourceJndiName" value="${database.datasource.jndiname}"/>
        </bean>                
    </property>
</bean>       

The hibernate.cfg.xml:

<hibernate-configuration>
    <session-factory>       
        <mapping class="info.ems.models.User" />
        <mapping class="info.ems.models.Role" />
    </session-factory>
</hibernate-configuration>

The Role.java:

@Entity
@Table(name="ROLE")
@Access(AccessType.FIELD)
public class Role implements Serializable {

    private static final long serialVersionUID = 3L;

    @Id
    @Column(name="ROLE_ID", updatable=false, nullable=false)
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private long id;

    @Column(name="USERNAME")
    private String username;

    @Column(name="ROLE")
    private String role;

    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 String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
}

And the User.java:

@Entity
@Table(name = "USER")
@Access(AccessType.FIELD)
public class User implements UserDetails, Serializable {

    private static final long serialVersionUID = 2L;

    @Id
    @Column(name = "USER_ID", updatable=false, nullable=false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    private String password;

    @Column(name = "NAME")
    private String name;

    @Column(name = "EMAIL")
    private String email;

    @Column(name = "LOCKED")
    private boolean locked;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = Role.class)
    @JoinTable(name = "USER_ROLE", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") })
    private Set<Role> roles;

    @Ove开发者_开发百科rride
    public GrantedAuthority[] getAuthorities() {
        List<GrantedAuthorityImpl> list = new ArrayList<GrantedAuthorityImpl>(0);
        for (Role role : roles) {
            list.add(new GrantedAuthorityImpl(role.getRole()));
        }
        return (GrantedAuthority[]) list.toArray(new GrantedAuthority[list.size()]);
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !isLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public long getId() {
        return id;
    }

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

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
}

The HibernateEMSDao has two methods for saving and loading User from database:

public void saveUser(final User user) {     
    getHibernateTemplate().execute(new HibernateCallback() {

        @Override
        public Object doInHibernate(Session session) throws HibernateException, SQLException {
            session.flush();
            session.setCacheMode(CacheMode.IGNORE);
            session.save(user);
            session.flush();
            return null;
        }
    });     
}

public User getUser(final Long id) {
    return (User) getHibernateTemplate().execute(new HibernateCallback() {

        @Override
        public Object doInHibernate(Session session) throws HibernateException, SQLException {
            return session.get(User.class, id);
        }
    });
}

Now I tested that if I implement HibernateEMSDao#getUser as:

public User getUser(final Long id) {
    getHibernateTemplate().load(User.class, id);        
}

I am getting LazyInitializationExcaption - session is closed. But the first way is working fine. So I need suggestion to avoid this exception in near future. Any small piece of information is appreciable.

Thanks and regards.

Note: I got that error after restarting my server.

Edit: code added:

public void saveUser(final User user) {     
    Session session = getSession();
    Transaction transaction = session.beginTransaction();
    session.save(user);
    transaction.commit();
    session.close();
}
public User getUser(final Long id) {
    Session session = getSession();
    session.enableFetchProfile("USER-ROLE-PROFILE");
    User user = (User) session.get(User.class, id);
    session.disableFetchProfile("USER-ROLE-PROFILE");
    session.close();
    return user;
}


Dealing with lazy loading is on ongoing challenge when working with Hibernate, JPA or ORMs in general.

It's not only about preventing the LazyInitializationException to happen, but also about doing queries efficiently. Even when using general DAOs a strategy should fetch as much as possible only the data that you really need.

The book Pro JPA 2 from Mike Keith by Apress dedicates a whole section on this, but there does not appear to be a universal solution that always works.

At times it can help to do FETCH joins. This does mean you don't use the entity manager's find method, but use JPQL (or HQL if that's your poison) queries for everything. Your DAOs can contain a few different methods that get the entity graph up to various levels this way. Data is typically fetched fairly efficiently this way, but for a lot of situations you might fetch too much data.

Another solution, suggested by Mike Keith as well, is taking advantage of the extended persistence context. In this case, the context (Hibernate session) is not bound to a transaction but remains open. Entities thus stay attached and lazy loading works as expected.

You have to make sure to eventually close the extended context though. One way of doing this is having it managed by a stateful session bean that's bound to some scope, e.g. the request scope or conversation scope. That way, the bean will be automatically destroyed at the end of this scope and this on its turn will automatically close the context.

It is however not without its own problems. An open context will continue to consume memory and keeping it open for a longer period (typically anything longer then request scope) might introduce serious risks for running out of memory. If you know you are only dealing with a handful of entities it's okay, but you have to be careful here.

Another problem with depending on lazy loading is the well known 1 + N query problem. Iterating over a result list of even medium size can result in hundreds or thousands of queries being send to the DB. I think I don't have to explain that this can completely destroy your performance.

This 1 + N query problem can sometimes be solved by relying heavily on second level caching. If the amount of entities is not that big and if they are not updated that frequently, making sure that they are all cached (using Hibernate's or JPA's 2nd level entity cache) can greatly reduce this problem. But... that are two big "if"s. And if your primary entity references only a single entity that is not cached, you'll get the hundreds of queries again.

Yet another approach is to take advantage of the fetch profile support in Hibernate, which can partially be combined with the other approaches. The reference manual has a section on this here: http://docs.jboss.org/hibernate/core/3.5/reference/en/html/performance.html#performance-fetching-profiles

There thus does not seem to be a single definite answer to your question, but only a lot of ideas and practices that are all highly dependent on your individual situation.


saveUser shouldn't flush the session. Flushing the session should really be rare. Let Hibernate take care of this, and your application will be more efficient.

Setting the cache mode at such a place is also really bizarre. Why do you do that?

As for the explanation about why you get an exception when using load and not when using get: it's because load assumes that you know that the entity exists. Rather than executing a select query to get the user data from the database, it just returns a proxy, which will get the data when a method is called for the first time on the object. If the session is closed when you call a method for the first time, Hibernate can't get the data anymore and throws the exception. load should be used rarely, except to initiate some relationship to an existing object without having to get its data. Use get in the other cases.

My general strategy to avoid LazyInitializationException is :

  • use attached objects whenever possible.
  • document which graph is loaded by the methods returning detached objects, and unit-test that this graph is indeed loaded
  • prefer merge over upadate and saveOrUpdate. These methods can leave a graph of objects with some objects attached, and others detached, depending on the cascades. merge doesn't suffer from this problem.
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜