如何修复Hibernate LazyInitializationException:无法延迟初始化角色集合,无法初始化代理 - 没有会话

199
在我的Spring项目中的自定义AuthenticationProvider中,我正在尝试读取已登录用户的权限列表,但是我遇到了以下错误:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.horariolivre.entity.Usuario.autorizacoes, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:566)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:186)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:545)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:124)
    at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:266)
    at com.horariolivre.security.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.java:45)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:177)
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:211)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:343)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:260)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1023)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)

阅读StackOverflow中的其他主题,我了解到这是由于框架处理此类型属性的方式导致的,但我无法找到任何解决方案。有人可以指出我做错了什么,以及我可以做什么来解决它吗?

我的自定义AuthenticationProvider代码如下:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UsuarioHome usuario;

    public CustomAuthenticationProvider() {
        super();
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        System.out.println("CustomAuthenticationProvider.authenticate");

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        Usuario user = usuario.findByUsername(username);

        if (user != null) {
            if(user.getSenha().equals(password)) {
                List<AutorizacoesUsuario> list = user.getAutorizacoes();

                List <String> rolesAsList = new ArrayList<String>();
                for(AutorizacoesUsuario role : list){
                    rolesAsList.add(role.getAutorizacoes().getNome());
                }

                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                for (String role_name : rolesAsList) {
                    authorities.add(new SimpleGrantedAuthority(role_name));
                }

                Authentication auth = new UsernamePasswordAuthenticationToken(username, password, authorities);
                return auth;
            }
            else {
                return null;
            }
        } else {
            return null;
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

我的实体类包括:

UsuarioHome.java

@Entity
@Table(name = "usuario")
public class Usuario implements java.io.Serializable {

    private int id;
    private String login;
    private String senha;
    private String primeiroNome;
    private String ultimoNome;
    private List<TipoUsuario> tipoUsuarios = new ArrayList<TipoUsuario>();
    private List<AutorizacoesUsuario> autorizacoes = new ArrayList<AutorizacoesUsuario>();
    private List<DadosUsuario> dadosUsuarios = new ArrayList<DadosUsuario>();
    private ConfigHorarioLivre config;

    public Usuario() {
    }

    public Usuario(String login, String senha) {
        this.login = login;
        this.senha = senha;
    }

    public Usuario(String login, String senha, String primeiroNome, String ultimoNome, List<TipoUsuario> tipoUsuarios, List<AutorizacoesUsuario> autorizacoesUsuarios, List<DadosUsuario> dadosUsuarios, ConfigHorarioLivre config) {
        this.login = login;
        this.senha = senha;
        this.primeiroNome = primeiroNome;
        this.ultimoNome = ultimoNome;
        this.tipoUsuarios = tipoUsuarios;
        this.autorizacoes = autorizacoesUsuarios;
        this.dadosUsuarios = dadosUsuarios;
        this.config = config;
    }

    public Usuario(String login, String senha, String primeiroNome, String ultimoNome, String tipoUsuario, String[] campos) {
        this.login = login;
        this.senha = senha;
        this.primeiroNome = primeiroNome;
        this.ultimoNome = ultimoNome;
        this.tipoUsuarios.add(new TipoUsuario(this, new Tipo(tipoUsuario)));
        for(int i=0; i<campos.length; i++)
            this.dadosUsuarios.add(new DadosUsuario(this, null, campos[i]));
    }

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy=GenerationType.AUTO)
    public int getId() {
        return this.id;
    }

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

    @Column(name = "login", nullable = false, length = 16)
    public String getLogin() {
        return this.login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    @Column(name = "senha", nullable = false)
    public String getSenha() {
        return this.senha;
    }

    public void setSenha(String senha) {
        this.senha = senha;
    }

    @Column(name = "primeiro_nome", length = 32)
    public String getPrimeiroNome() {
        return this.primeiroNome;
    }

    public void setPrimeiroNome(String primeiroNome) {
        this.primeiroNome = primeiroNome;
    }

    @Column(name = "ultimo_nome", length = 32)
    public String getUltimoNome() {
        return this.ultimoNome;
    }

    public void setUltimoNome(String ultimoNome) {
        this.ultimoNome = ultimoNome;
    }

    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name = "tipo_usuario", joinColumns = { @JoinColumn(name = "fk_usuario") }, inverseJoinColumns = { @JoinColumn(name = "fk_tipo") })
    @LazyCollection(LazyCollectionOption.TRUE)
    public List<TipoUsuario> getTipoUsuarios() {
        return this.tipoUsuarios;
    }

    public void setTipoUsuarios(List<TipoUsuario> tipoUsuarios) {
        this.tipoUsuarios = tipoUsuarios;
    }

    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name = "autorizacoes_usuario", joinColumns = { @JoinColumn(name = "fk_usuario") }, inverseJoinColumns = { @JoinColumn(name = "fk_autorizacoes") })
    @LazyCollection(LazyCollectionOption.TRUE)
    public List<AutorizacoesUsuario> getAutorizacoes() {
        return this.autorizacoes;
    }

    public void setAutorizacoes(List<AutorizacoesUsuario> autorizacoes) {
        this.autorizacoes = autorizacoes;
    }

    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name = "dados_usuario", joinColumns = { @JoinColumn(name = "fk_usuario") }, inverseJoinColumns = { @JoinColumn(name = "fk_dados") })
    @LazyCollection(LazyCollectionOption.TRUE)
    public List<DadosUsuario> getDadosUsuarios() {
        return this.dadosUsuarios;
    }

    public void setDadosUsuarios(List<DadosUsuario> dadosUsuarios) {
        this.dadosUsuarios = dadosUsuarios;
    }

    @OneToOne
    @JoinColumn(name="fk_config")
    public ConfigHorarioLivre getConfig() {
        return config;
    }

    public void setConfig(ConfigHorarioLivre config) {
        this.config = config;
    }
}

AutorizacoesUsuario.java

@Entity
@Table(name = "autorizacoes_usuario", uniqueConstraints = @UniqueConstraint(columnNames = "id"))
public class AutorizacoesUsuario implements java.io.Serializable {

    private int id;
    private Usuario usuario;
    private Autorizacoes autorizacoes;

    public AutorizacoesUsuario() {
    }

    public AutorizacoesUsuario(Usuario usuario, Autorizacoes autorizacoes) {
        this.usuario = usuario;
        this.autorizacoes = autorizacoes;
    }

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy=GenerationType.AUTO)
    public int getId() {
        return this.id;
    }

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

    @OneToOne
    @JoinColumn(name = "fk_usuario", nullable = false, insertable = false, updatable = false)
    public Usuario getUsuario() {
        return this.usuario;
    }

    public void setUsuario(Usuario usuario) {
        this.usuario = usuario;
    }

    @OneToOne
    @JoinColumn(name = "fk_autorizacoes", nullable = false, insertable = false, updatable = false)
    public Autorizacoes getAutorizacoes() {
        return this.autorizacoes;
    }

    public void setAutorizacoes(Autorizacoes autorizacoes) {
        this.autorizacoes = autorizacoes;
    }

}

Autorizacoes.java

@Entity
@Table(name = "autorizacoes")
public class Autorizacoes implements java.io.Serializable {

    private int id;
    private String nome;
    private String descricao;

    public Autorizacoes() {
    }

    public Autorizacoes(String nome) {
        this.nome = nome;
    }

    public Autorizacoes(String nome, String descricao) {
        this.nome = nome;
        this.descricao = descricao;
    }

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy=GenerationType.AUTO)
    public int getId() {
        return this.id;
    }

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

    @Column(name = "nome", nullable = false, length = 16)
    public String getNome() {
        return this.nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    @Column(name = "descricao", length = 140)
    public String getDescricao() {
        return this.descricao;
    }

    public void setDescricao(String descricao) {
        this.descricao = descricao;
    }
}

完整项目可在Github上获取

--> https://github.com/klebermo/webapp_horario_livre


迫不及待地获取您的权限,或使用OpenSessionInViewFilter。 - Bart
这正是我试图寻找如何做的内容。我尝试过的是:**List<Autorizacoes> authority = user.getAutorizacoes()**,在分配UsernamePasswordAuthenticationToken的同一函数内,但仍然不起作用。 - Kleber Mota
2
@ManyToMany(cascade=CascadeType.ALL, fetch = FetchType.EAGER) - Bart
好的,我试过了,但还是不行。我的实体类已经更新:https://github.com/klebermo/webapp_horario_livre/blob/master/src/com/horariolivre/entity/Usuario.java,我的当前身份验证提供程序为:https://github.com/klebermo/webapp_horario_livre/blob/master/src/com/horariolivre/security/CustomAuthenticationProvider.java。 - Kleber Mota
可能是重复的问题,参考如何解决“failed to lazily initialize a collection of role” Hibernate异常 - Martin Schröder
19个回答

234
您需要在ManyToMany注解中添加fetch=FetchType.EAGER以自动获取子实体。
@ManyToMany(fetch = FetchType.EAGER)

更好的选择是通过将以下内容添加到您的Spring配置文件中来实现一个Spring transactionManager
<bean id="transactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:annotation-driven />

你可以在你的authenticate()方法上添加一个@Transactional注解,像这样:
@Transactional
public Authentication authenticate(Authentication authentication)

这将在整个authenticate方法的持续时间内启动一个数据库事务,允许在您尝试使用它们时从数据库中检索任何延迟加载的集合。

1
实际上,我已经在我的应用程序中配置了transactionManager,并在我的DAO类中使用它。如果我尝试像你建议的那样在AuthenticationProvider的authenticate方法中使用它,我会收到一个错误Caused by: java.lang.IllegalArgumentException: Can not set com.horariolivre.security.CustomAuthenticationProvider field com.horariolivre.security.SecurityConfig.authenticationProvider to $Proxy36。如果我在我的ManyToMany注释中使用add fetchType=FetchType.EAGER,我会得到相同的错误(而且我只能在一个属性中使用这个 - 我的Entity类Usuario中有三个相同类型的属性)。 - Kleber Mota
3
为了避免LazyInitializationException,您需要遍历要在Transaction中使用的子实体。由于您的事务注释位于通用方法的dao级别上,因此您可能不想在那里执行此操作,因此您需要在dao前面实现一个服务类,该类具有@Transactional边界,您可以在其中遍历所需的子实体。 - jcmwright80
7
如果未来有人遇到这个问题,提示如下:@Transaction 注解需要放在公共方法上。如果不是,它将无法正常工作。可能会有警告,也可能没有。 - Nicolas
使用了fetch type,它完美地工作了。问题是使用急切的fetch和@transactional对应部分有什么区别? - Austine Gwa
3
主要的区别是在联接中添加急切加载(fetchType)选项会导致每当主实体被加载时,子实体列表都将从数据库中返回,因此如果只需要主实体数据的某些功能存在,则可能会对性能造成影响,因此使用事务和延迟加载可以更好地控制所获取数据的数量,但完全取决于您的应用程序和用例来确定哪种方法是正确的。 - jcmwright80
1
@Vlad Mihalcea提出了最佳解决方案,但在某些情况下,这种解决方案可能会导致多次查询,这是一种反模式。 - Javier

63

1
Fetch Join 相当于 Eager Fetching,但这种方式并不总是可行或高效。此外,获取对象的通常方式并不是通过 JPQL 查询。关于“开放式会话视图是反模式”的说法有些牵强附会,而且老实说,我不同意。显然应该谨慎使用,但有许多完全合理的用例可以从中受益。 - fer.marino
8
不,这是不正确的。在视图中打开会话是一种不规范的做法,它表明即使是只针对只读投影也会获取实体。无论你如何努力为其辩护,不存在许多完全可以使用它受益的用例这样的事情。没有理由获取比您真正需要的更多数据,也没有理由泄漏数据提取超出事务服务层的边界。 - Vlad Mihalcea
你好 Vlad, 能否请您解释一下为什么FETCH JOIN不等同于eager loading。我正在阅读这篇文章:http://blog.arnoldgalovics.com/2017/02/27/lazyinitializationexception-demystified/。它说:“更好的方法是在加载父实体(公司)时同时加载关系。这可以通过Fetch Join来完成。”所以这是一种eager loading,不是吗? - Geek
4
热心的引导意味着将FetchType.EAGER添加到你的关联实体中。JOIN FETCH用于需要在查询时急切获取的FetchType.LAZY关联实体。 - Vlad Mihalcea
嗨,弗拉德,如果我们使用Spring JpaRepository而不编写显式查询,我们可以执行JOIN FETCH吗? - ssc327
实体图也是一种选择,但自定义存储库是更好的解决方案。 - Vlad Mihalcea

34

在进行单元测试时,我也遇到了这个问题。解决这个问题的一个非常简单的方法是使用 @Transactional 注释,它会保持会话一直开启直到执行结束。


你正在使用Hibernate事务还是JPA事务? - wheeleruniverse
1
我使用了Hibernate。 - KarthikaSrinivasan

29

将以下属性添加到您的persistence.xml中,可能会暂时解决您的问题。

<property name="hibernate.enable_lazy_load_no_trans" value="true" />

正如 @vlad-mihalcea 所说,这是一种反模式,并不能完全解决延迟初始化问题,请在关闭事务之前初始化您的关联,并使用 DTOs。


这被 Hibernate 专家认为是一种反模式。https://stackoverflow.com/users/1025118/vlad-mihalcea - Zon

15
使用懒加载时会关闭会话,因此有两种解决方案:
1. 不使用懒加载。在XML中设置lazy=false或在注释中设置@OneToMany(fetch = FetchType.EAGER)
2. 使用懒加载。在XML中设置lazy=true或在注释中设置@OneToMany(fetch = FetchType.LAZY),并在web.xml中添加OpenSessionInViewFilter filter
详见文章:https://dev59.com/7mYr5IYBdhLWcg3wcJx0#27286187

2
OpenSessionInViewFilter也是一种反模式。我还建议永远不要将映射设置为EAGER,因为有很多情况下你不需要EAGER集合中的数据,而且你会拉取比这些用例需要的更多的数据,从而大大降低性能。请保持所有映射为LAZY,并在查询中添加join fetches。 - bytor99999
1
"并将联接获取添加到您的查询中。这是什么意思?" - Daniel Methner

14
您的自定义身份验证提供程序类应该用以下注释进行注释:

@Transactional

这将确保在那里存在 hibernate 会话。

在我的Service类上添加@Transactional修饰符解决了这个问题。非常感谢。 - lmarqs
很高兴能帮到你! - Bilal Ahmed Yaseen

9

对于那些遇到枚举集合问题的人,这是解决方法:

@Enumerated(EnumType.STRING)
@Column(name = "OPTION")
@CollectionTable(name = "MY_ENTITY_MY_OPTION")
@ElementCollection(targetClass = MyOptionEnum.class, fetch = EAGER)
Collection<MyOptionEnum> options;

这对我有效。我还测试了添加 @Transactional 选项,它也有效。但我选择了这个选项。 - rick dana

7

一种常见的做法是在服务类上方放置@Transactional

@Service
@Transactional
public class MyServiceImpl implements MyService{
...
}

6
您可以使用Hibernate的延迟初始化器。以下是您可以参考的代码。
这里的PPIDO是我想要检索的数据对象。
Hibernate.initialize(ppiDO);
if (ppiDO instanceof HibernateProxy) {
    ppiDO = (PolicyProductInsuredDO) ((HibernateProxy) ppiDO).getHibernateLazyInitializer()
        .getImplementation();
    ppiDO.setParentGuidObj(policyDO.getBasePlan());
    saveppiDO.add(ppiDO);
    proxyFl = true;
}

5

有些情况下,你不需要在服务方法上加上@Transactional注释,比如集成测试中,可以只将@Transactional 添加到测试方法中。当测试一个只从数据库中选择数据的方法时,你可能会遇到org.hibernate. LazyInitializationException异常,这个方法并不需要是事务性的。例如,当你尝试加载一个具有延迟获取关联的实体类时,就像下面这样,可能会导致这个异常:

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Item> items;

因此,您只需将@Transactional注释添加到测试方法中即可。

@Test
@Transactional
public void verifySomethingTestSomething()  {

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