【问题标题】:Architecture to avoid Hibernate LazyInitializationExceptions避免 Hibernate LazyInitializationExceptions 的架构
【发布时间】:2011-05-29 13:07:29
【问题描述】:

我的项目刚刚开始。所以我正在尝试设计一种避免 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>

角色.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);        
}

我得到 LazyInitializationExcaption - 会话已关闭。但第一种方法工作正常。所以我需要建议在不久的将来避免这种异常。任何小的信息都是可观的。

谢谢和问候。

注意:我在重新启动服务器后遇到了该错误。

编辑:添加代码:

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

【问题讨论】:

    标签: java hibernate lazy-initialization


    【解决方案1】:

    在使用 Hibernate、JPA 或 ORM 时,处理延迟加载一直是个挑战。

    这不仅是为了防止 LazyInitializationException 发生,而且是为了有效地进行查询。即使在使用通用 DAO 时,策略也应该尽可能只获取您真正需要的数据。

    Apress 出版的 Mike Keith 的书 Pro JPA 2 专门用了一整节来讨论这个问题,但似乎没有一个始终有效的通用解决方案。

    有时它可以帮助进行 FETCH 连接。这确实意味着您不使用实体管理器的 find 方法,而是对所有内容使用 JPQL(或 HQL,如果那是您的毒药)查询。你的 DAO 可以包含一些不同的方法,以这种方式将实体图提升到不同的级别。这种方式通常可以相当有效地获取数据,但在很多情况下,您可能会获取太多数据。

    Mike Keith 也提出了另一个解决方案,即利用extended persistence context。在这种情况下,上下文(Hibernate 会话)没有绑定到事务,而是保持打开状态。实体因此​​保持连接并且延迟加载按预期工作。

    您必须确保最终关闭扩展上下文。一种方法是让它由绑定到某个范围的有状态会话 bean 管理,例如请求范围或会话范围。这样,bean 将在此范围结束时自动销毁,而 this 将自动关闭上下文。

    然而,它并非没有自己的问题。开放的上下文将继续消耗内存并使其保持开放更长的时间(通常比请求范围更长)可能会导致内存耗尽的严重风险。如果您知道您只与少数实体打交道,那没关系,但在这里您必须小心。

    依赖延迟加载的另一个问题是众所周知的 1 + N 查询问题。对中等大小的结果列表进行迭代可能会导致数百或数千个查询被发送到数据库。我想我不必解释这会完全破坏你的表现。

    这种 1 + N 查询问题有时可以通过严重依赖二级缓存来解决。如果实体的数量不是那么大并且它们没有那么频繁地更新,那么确保它们都被缓存(使用 Hibernate 或 JPA 的 2 级实体缓存)可以大大减少这个问题。但是……那是两个大的“如果”。如果您的主要实体仅引用一个未缓存的实体,您将再次获得数百个查询。

    还有一种方法是利用 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 处理这个,你的应用程序会更有效率。"我应该为此做任何特殊设置吗?
      • 我在 Internet 上找到了一个示例,其中我看到了这种类型的操作,例如第一个 session.flush(),然后是缓存模式操作,最后是 flush()。
      • 不要相信您在 Internet 上找到的所有内容。阅读参考手册:docs.jboss.org/hibernate/core/3.6/reference/en-US/html_single。默认的flush模式会在需要flush时自动flush。
      • 我已经添加了上述两个方法saveUser和getUser,我正在阅读那篇文章。这是更好的方法吗?
      猜你喜欢
      • 1970-01-01
      • 2011-02-23
      • 1970-01-01
      • 1970-01-01
      • 2011-09-11
      • 2021-11-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多