避免 Hibernate LazyInitializationExceptions 的架构

发布于 2024-11-10 01:37:03 字数 7875 浏览 0 评论 0原文

我正处于我的项目的开始阶段。所以我试图设计一个避免 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);        
}

我收到 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;
}

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;

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

当爱已成负担 2024-11-17 01:37:03

一般来说,在使用 Hibernate、JPA 或 ORM 时,处理延迟加载是一个持续的挑战。

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

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

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

Mike Keith 也建议了另一种解决方案,即利用扩展持久性上下文。在这种情况下,上下文(Hibernate 会话)不绑定到事务,而是保持打开状态。因此,实体保持连接状态并且延迟加载按预期工作。

不过,您必须确保最终关闭扩展上下文。实现此目的的一种方法是由绑定到某个范围(例如请求范围或会话范围)的有状态会话 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

因此,似乎没有一个明确的答案你的问题,但只有很多想法和实践都高度依赖于你的个人情况。

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.

顾挽 2024-11-17 01:37:03

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

在这种地方设置缓存模式也确实很奇怪。你为什么这么做?

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

我避免 LazyInitializationException 的一般策略是:

  • 尽可能使用附加对象。
  • 记录返回分离对象的方法加载了哪个图,并且对该图确实加载的单元测试
  • 更喜欢 merge 而不是 upadatesaveOrUpdate。这些方法可以留下对象图,其中一些对象被附加,另一些对象被分离,具体取决于级联。 merge 不会遇到这个问题。

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.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文