避免Hibernate LazyInitializationExceptions的架构

3

我正在开始我的项目。因此,我试图设计一种架构,避免Hibernate LazyInitializationExceptions的出现。到目前���止,我的applicationContext.xml文件中有以下内容:

<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>       

hibernate.cfg.xml文件:

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

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;
    }
}

还有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;

    @Override
    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;
    }
}

HibernateEMSDao有两个方法用于保存和从数据库加载用户信息:
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);
        }
    });
}

现在我测试了一下,如果我将HibernateEMSDao#getUser实现为:

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

我得到了LazyInitializationException - session is closed的错误。但第一种方法却运行良好。因此,我需要建议以避免在不久的将来出现此异常。任何小片段的信息都是可赞赏的。
感谢和问候。
注:我在重新启动服务器后收到该错误。
编辑:代码已添加:
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;
}
2个回答

4
处理懒加载是在使用Hibernate、JPA或ORM等技术时面临的持续挑战。这不仅涉及防止出现LazyInitializationException,还包括高效地执行查询。即使使用通用DAO,策略也应该尽可能地仅获取实际需要的数据。Mike Keith所著的Apress书籍《Pro JPA 2》专门讨论了这个问题,但似乎没有一种通用的解决方案始终适用。有时可以使用FETCH连接来帮助解决这个问题。这意味着您不使用实体管理器的查找方法,而是针对所有内容使用JPQL(或HQL)查询。通过这种方式,您的DAO可以包含几种不同的方法,以将实体图形提升到各个级别。通过这种方式通常可以相当有效地获取数据,但对于许多情况,您可能会获取过多的数据。Mike Keith建议的另一种解决方案是利用“扩展持久性上下文”。在这种情况下,上下文(Hibernate会话)未绑定到事务,而保持打开状态。因此,实体保持附加状态,并且懒加载按预期工作。
你必须确保最终关闭扩展上下文。一种方法是将其由有状态会话bean管理,该bean绑定到某个范围,例如请求范围或对话范围。这样,bean将在此范围结束时自动销毁,并且这将自动关闭上下文。
然而,这也存在问题。打开的上下文将继续消耗内存,并且长时间保持打开(通常超过请求范围)可能会引入严重的内存不足风险。如果你知道只处理少量实体,那就没问题,但这里需要小心。
依赖延迟加载的另一个问题是众所周知的1 + N查询问题。即使是中等大小的结果列表迭代也可能导致发送数百或数千个查询到数据库。我想我不需要解释这可能完全破坏你的性能。
这个1+N的查询问题有时可以通过大量依赖第二级缓存来解决。如果实体数量不是很大,而且更新频率不高,则确保它们都被缓存(使用Hibernate或JPA的第二级实体缓存)可以极大地减少这个问题。但是...这是两个重要的“如果”。如果您的主实体仅引用一个未缓存的单个实体,您将再次获得数百个查询。

另一种方法是利用Hibernate中的fetch profile支持,它可以部分地与其他方法结合使用。参考手册在这里有对此的介绍: http://docs.jboss.org/hibernate/core/3.5/reference/en/html/performance.html#performance-fetching-profiles

因此,似乎没有一个单一确定的答案来回答您的问题,只有许多思路和做法,这些都高度依赖于您的个人情况。

2

saveUser 不应该刷新会话。刷新会话应该非常少见。让 Hibernate 来处理这个问题,你的应用程序将更加高效。

在这样的地方设置缓存模式也真的很奇怪。你为什么要这样做?

至于为什么使用 load 而不是使用 get 时会出现异常的解释:这是因为 load 假定您知道实体存在。它不执行选择查询以从数据库中获取用户数据,而是返回一个代理,在对象上第一次调用方法时获取数据。如果在第一次调用方法时关闭了会话,则 Hibernate 无法再获取数据并抛出异常。load 应该很少使用,除非需要初始化到现有对象的某些关系而无需获取其数据。在其他情况下,请使用 get

我避免 LazyInitializationException 的一般策略是:

  • 尽可能使用附加的对象。
  • 记录由返回分离对象的方法加载的图,并单元测试确保已加载此图
  • 优先使用 merge 而不是 upadatesaveOrUpdate。这些方法可以使对象图形具有一些附加的对象和其他分离的对象,取决于级联。 merge 不会出现这个问题。

让Hibernate来处理这个问题,你的应用程序将更加高效。我需要进行任何特殊设置吗? - Tapas Bose
我在互联网上找到了一个例子,其中我看到了这种操作,首先是session.flush(),然后是缓存模式操作,最后再次flush()。 - Tapas Bose
不要相信你在互联网上找到的所有东西。阅读参考手册:http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html_single/。默认刷新模式会在需要刷新时自动刷新。 - JB Nizet
我已经添加了上述两种方法saveUser和getUser,现在正在阅读那篇文章。这是更好的方式吗? - Tapas Bose

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接