JSF + Hibernate + Lazy Collection

14 respostas
tebosoftware

Bom dia pessoal

Estou com um problema que eu já procurei em muitos lugares as soluções que eles me passaram não deu certo.
A última foi implementar o filtro abaixo:

package br.com.tebosoftware.web;

import br.com.tebosoftware.conexao.HibernateUtil;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.context.internal.ManagedSessionContext;

/**
 *
 * @author Thales
 */
@WebFilter(filterName = "ConexaoFiltro", urlPatterns = {"*"})
public class ConexaoFiltro implements Filter {

    private SessionFactory sf;
    public static final String HIBERNATE_SESSION_KEY = "hibernateSession";
    public static final String END_OF_CONVERSATION_FLAG = "endOfConversation";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpSession httpSession = httpServletRequest.getSession();
        Session currentSession;
        Session disconnectedSession =
                (Session) httpSession.getAttribute(HIBERNATE_SESSION_KEY);
        try {

            // Start a new conversation or in the middle?
            if (disconnectedSession == null) {
                currentSession = sf.openSession();
                currentSession.setFlushMode(FlushMode.MANUAL);
            } else {
                currentSession = (Session) disconnectedSession;
            }

            ManagedSessionContext.bind(currentSession);

            if (!currentSession.getTransaction().isActive()) {
                currentSession.getTransaction().begin();
            }

            chain.doFilter(request, response);

            currentSession = ManagedSessionContext.unbind(sf);

            // End or continue the long-running conversation?
            if (request.getAttribute(END_OF_CONVERSATION_FLAG) != null
                    || request.getParameter(END_OF_CONVERSATION_FLAG) != null) {
                currentSession.flush();
                currentSession.getTransaction().commit();
                currentSession.close();
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);
            } else {
                currentSession.getTransaction().commit();
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, currentSession);
            }

        } catch (StaleObjectStateException staleEx) {
            // Rollback, close everything, possibly compensate for any permanent changes
            // during the conversation, and finally restart business conversation. Maybe
            // give the user of the application a chance to merge some of his work with
            // fresh data... what you do here depends on your applications design.
            throw staleEx;
        } catch (Throwable ex) {
            // Rollback only
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    sf.getCurrentSession().getTransaction().rollback();
                }
            } catch (Throwable rbEx) {
                //log.error("Could not rollback transaction after exception!", rbEx);
            } finally {
                currentSession = ManagedSessionContext.unbind(sf);
                currentSession.close();
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);

            }

            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        try {
            sf = HibernateUtil.getSessionFactory();
        } catch (Throwable ex) {
            throw new ServletException("Falha ao inicializar o banco de dados", ex);
        }
    }

    @Override
    public void destroy() {
    }
}

só que continua dando erro ao fazer o load da classe abaixo:

package br.com.tebosoftware.modelo.acesso;

import br.com.tebosoftware.modelo.AbstractModelo;
import br.com.tebosoftware.modelo.ValidacaoException;
import br.com.tebosoftware.util.Util;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

/**
 *
 * @author thales @data 05/03/2012
 */
@Entity
public class Perfil extends AbstractModelo {

    public static final int LENGTH_DESCRICAO = 50;
    public static final int LENGTH_PERMISSAO = 100;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Column(length = LENGTH_DESCRICAO)
    private String descricao;
    @ManyToMany(cascade={CascadeType.ALL})
    private List<Permissao> permissoes;

    public String getDescricao() {
        return descricao;
    }

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

    public Integer getId() {
        return id;
    }

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

    public List<Permissao> getPermissoes() {
        return permissoes;
    }

    public void setPermissoes(List<Permissao> permissoes) {
        this.permissoes = permissoes;
    }

    @Override
    public Serializable getPrimaryKey() {
        return id;
    }

    @Override
    public void validar() throws ValidacaoException {
        List<String> erros = new ArrayList<String>();
        if (Util.isNull(descricao)) {
            erros.add("Descrição é obrigatória");
        }
        if (erros.size() > 0) {
            throwValidationException(erros);
        }
    }
}

que no caso ela tem um ManyToMany para a classe Permissão

package br.com.tebosoftware.modelo.acesso;

import br.com.tebosoftware.modelo.AbstractModelo;
import br.com.tebosoftware.modelo.ValidacaoException;
import br.com.tebosoftware.util.Util;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

/**
 *
 * @author thales @data 06/03/2012
 */
@Entity
public class Permissao extends AbstractModelo {

    public static final int LENGTH_CHAVE = 20;
    public static final int LENGTH_DESCRICAO = 50;
    @Id
    @Column(length = LENGTH_CHAVE)
    private String chave;
    @Column(length = LENGTH_DESCRICAO)
    private String descricao;

    public String getChave() {
        return chave;
    }

    public void setChave(String chave) {
        this.chave = Util.limitarString(chave, LENGTH_CHAVE);
    }

    public String getDescricao() {
        return descricao;
    }

    public void setDescricao(String descricao) {
        this.descricao = Util.limitarString(descricao, LENGTH_DESCRICAO);
    }

    @Override
    public Serializable getPrimaryKey() {
        return chave;
    }

    @Override
    public void validar() throws ValidacaoException {
        List<String> erros = new ArrayList<String>();
        if (Util.isNull(chave)) {
            erros.add("Chave é obrigatória");
        }
        if (Util.isNull(descricao)) {
            erros.add("Descrição é obrigatória");
        }
        if (erros.size() > 0) {
            throwValidationException(erros);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Permissao other = (Permissao) obj;
        if ((this.chave == null) ? (other.chave != null) : !this.chave.equals(other.chave)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 11 * hash + (this.chave != null ? this.chave.hashCode() : 0);
        hash = 11 * hash + (this.descricao != null ? this.descricao.hashCode() : 0);
        return hash;
    }
}

Na minha view fica assim:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:body>
        <p:focus for="txtDescricaoReg" rendered="#{perfilBean.novoModelo}" />
        <h:panelGrid columns="2">
            <h:outputLabel for="txtDescricaoReg" value="Descrição (*):" />
            <p:inputText id="txtDescricaoReg" value="#{perfilBean.modelo.descricao}" 
                         size="25" maxlength="50" 
                         onkeypress="keyUpperCase(event)" />
            <h:outputLabel for="chkPermissoes" value="Permissões (*):" />
            <p:selectManyCheckbox id="chkPermissoes" 
                                  value="#{perfilBean.modelo.permissoes}"
                                  layout="pageDirection">
                <f:selectItems var="permissao" value="#{perfilBean.permissoes}" 
                               itemValue="#{permissao}" itemLabel="#{permissao.descricao}" />
                <f:converter converterId="modeloConverter" />
            </p:selectManyCheckbox>
            <p:commandButton value="Gravar" action="#{perfilBean.gravar()}" update="cadastro" />
            <p:commandButton value="Cancelar" action="#{perfilBean.cancelar()}" update="cadastro" immediate="true" />
        </h:panelGrid>
    </h:body>
</html>

Só que exibir ele até exibe, mas na hora de gravar da o erro de org.hibernate.LazyInitializationException.

Alguém pode me ajudar?

14 Respostas

alias

realmente, esse filtro (que implementa o OpenSessionInView) serve pra manter a sessão aberta DURANTE o processamento da request. No momento em que voce clica ai no “Salvar”, a sessão vinculada à entidade já foi pro saco. É o que vai acontecer mesmo.

Receio que você terá que obter novamente a entidade da sua base.

tebosoftware

O problema é que essa parte seria no restore da view e isso é um problema.
Eu pensei que já que a lista já está obtida, não deveria dar o erro.

Uma solução que eu encontrei é fazer assim:

...
@Entity
public class Perfil extends AbstractModelo {

...
    public List<Permissao> getPermissoes() {
        if (permissoes != null && ! (permissoes instanceof ArrayList)) {
            permissoes = new ArrayList<Permissao>(permissoes);
        }
        return permissoes;
    }
}

Até funciona, só achei que é errado.

Como você faz para restaurar? O meu Bean está no modo @ViewScoped.

alias

Algum ponto da sua pagina usa a tal coleção lazy? Se não, entao ela não vai estar inicializada mesmo…

tebosoftware

Sim usa, pois no caso eu tenho um selectManyCheckbox e nele tem vínculo com a lista de permissões. e é nesse ponto que da o erro.

alias

o erro está ocorrendo no momento da renderização da pagina,entao?

estranho, pois o seu filtro lá é justamente pra resolver esse cenario…na classe em que voce recupera os dados do banco, em algum DAO da vida aí, você está fechando a sessão explicitamente?

tebosoftware

O erro não está na renderização e sim na restauração da view. Os dados foram exibidos no browse mas na hora do submit da o erro.

alias

desculpe, não entendi…na “restauração” da view? voce clica em salvar, ele executa o metodo e depois dá o erro? ou quando voce clica é o momento do xabu?

veja, como voce implementou o filtro do hibernate lá, na primeira renderização a sua lista será carregada do banco. o estado dessa sua entidade está sendo mantido no managed-bean? voce está refazendo a consulta? com as informações que voce postou, pode-se concluir que essa lista estará carregada quando a action do botao “salvar” for invocada, supondo que é a mesma entidade que voce usou pra renderizar a pagina.

tebosoftware

Quando eu tento debugar, não chega nem a chegar no método. O url pattern do filtro é /* e o do jsf é .tebo
então eu suponho que o /
seria primeiro do que o do jsf não?

alias

tebosoftware:
Quando eu tento debugar, não chega nem a chegar no método. O url pattern do filtro é /* e o do jsf é .tebo
então eu suponho que o /
seria primeiro do que o do jsf não?

a ordem de execução dos filtros é a ordem declarada no web.xml (no caso, o filtro do OpenSessionInView deve ser o primeirão)

tebosoftware

Então eu coloquei os filtros assim:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Production</param-value>
    </context-param>
    <context-param>
        <param-name>facelets.SKIP_COMMENTS</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
        <param-value>.xhtml</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>casablanca</param-value>
    </context-param>
    <filter>
        <filter-name>ConexaoFiltro</filter-name>
        <filter-class>br.com.tebosoftware.web.ConexaoFiltro</filter-class>
    </filter>
    <filter>
        <filter-name>CharsetFilter</filter-name>
        <filter-class>br.com.tebosoftware.web.CharsetFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ConexaoFiltro</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharsetFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.tebo</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.tebo</welcome-file>
    </welcome-file-list>
    <error-page>
        <exception-type>javax.faces.application.ViewExpiredException</exception-type>
        <location>/sessionExpired.xhtml</location>
    </error-page>
</web-app>
alias

Hm, repare que voce colocou o filtro no web.xml e tambem anotou a classe com WebFilter. Não conheço a especificação Servlet 3, não sei dizer qual teria preferencia…como o cara já está no xml, não quer tentar arrancar essa anotação dai? ou fazer o contrário…

Ademais, o urlPattern da anotação @WebFilter tambem precisa ser /*

Cheque esses detalhes aí e vamos fazer esse filtro rodar, e o seu problema do lazy estará resolvido. :wink:

tebosoftware

Esse foi só um teste que eu fiz. Eu comentei a anotação e coloquei no web.xml. Então continua a ter somente em um lugar a notação.

alias

voce manteve apenas a anotação, tirou do web.xml, é isso?

tebosoftware

ao contrario, tirei a anotação e deixei somente no web.xml

Criado 7 de março de 2012
Ultima resposta 8 de mar. de 2012
Respostas 14
Participantes 2