Problema com múltiplos joins em Criteria

November 12th, 2009 by guilhermegarnier Leave a reply »

O Criteria é uma API do Hibernate que facilita muito quando precisamos montar uma query complexa com filtros opcionais. Adicionar restrições ou criar joins com esta API é muito mais simples de gerenciar do que concatenando Strings, como faríamos ao trabalhar com SQL puro.

Apesar das vantagens, o Criteria também tem alguns problemas. O último que encontrei foi ao tentar fazer 2 joins entre as mesmas 2 tabelas. No meu caso, eu tinha no banco as tabelas projeto e historico. A segunda tabela é populada através de uma trigger no banco: sempre que o status do projeto muda, a tabela historico registra o status anterior do projeto com data/hora da mudança. Eu precisava fazer uma query que buscasse um projeto com status “iniciado” num determinado período de datas e com status “finalizado” em outro período. Inicialmente, pensei simplesmente em criar 2 joins entre as tabelas, cada um com um alias diferente e filtrando pelas datas específicas:

Criteria criteria = getSession().createCriteria(Projeto.class);

// Primeiro join
criteria.createCriteria("historicoList", "historicoIniciado", Criteria.LEFT_JOIN)
        .add(Restrictions.eq("historicoIniciado.status", Status.INICIADO.value()))
        .add(Restrictions.ge("historicoIniciado.data", dataIniciadoDe))
        .add(Restrictions.le("historicoIniciado.data", dataIniciadoAte));

// Segundo join
criteria.createCriteria("historicoList", "historicoFinalizado", Criteria.LEFT_JOIN)
        .add(Restrictions.eq("historicoFinalizado.status", Status.FINALIZADO.value()))
        .add(Restrictions.ge("historicoFinalizado.data", dataFinalizadoDe))
        .add(Restrictions.le("historicoFinalizado.data", dataFinalizadoAte));

O código acima, apesar de semelhante ao que eu já havia criado para adicionar outros filtros à query de projetos, fazendo joins com outras tabelas, não funcionava. Tentei retirar um dos joins com a tabela historico e funcionou. Ou seja, o problema estava na criação do segundo join com as mesmas tabelas, mesmo utilizando aliases diferentes. Ao pesquisar este problema, descobri que não é um bug. Na verdade, o Criteria não suporta múltiplos joins para a mesma associação.

Sendo assim, a solução que encontrei para este problema foi criar uma subquery para a tabela historico, utilizando um DetachedCriteria:

DetachedCriteria historicoCriteria = DetachedCriteria.forClass(Historico.class, "historicoIniciado")
        .setProjection(Projections.distinct(Projections.property("projeto")))
        .add(Restrictions.eq("historicoIniciado.status", Status.INICIADO.value()));
        .add(Restrictions.ge("historicoIniciado.data", dataIniciadoDe));
        .add(Restrictions.le("historicoIniciado.data", dataIniciadoAte));
criteria.add(Subqueries.propertyIn("id", historicoCriteria));

Desta forma, apenas um dos joins precisa ser substituído por uma subquery. O outro join pode ser mantido sem problemas.

Posts relacionados:

7 comments

  1. Ânderson Soares says:

    Fala meu amigo.

    Não sei se meu problema era semelhante ao seu, mas vou postar uma solução, em que consegui fazer o JOIN entre 4 tabelas.
    Segue:

    Eu tenho as tabelas, vou colocar so os dados que interessam
    – EntrevistadorPesquisa(id, (FK)idPesquisa, (FK)idEntrevistador)
    – Entrevistador(id, (FK)idUsuario, (FK)idNivel)
    – Usuario(id)
    – NivelEntrevistador(id, descricao)
    ps: todos id’s são P.Keys.
    ps: 2, tudo que for CAIXA ALTA a partir de agora é tabela

    Sendo assim, eu acesso a tabela ENTREVISTADOR_PESQUISA, pra saber quais entrevistadores estão relacionados a quais pesquisas.
    Preciso então cruzar essa ultima com ENTREVISTADOR, pra ter os dados do entrevistador. Preciso cruzar ENTREVISTADOR com USUARIO, para eu ter acesso a dados de login, senha, ativo…etc.
    E preciso cruzar ENTREVISTADOR com NIVEL_ENTREVISTADOR, pra saber qual o nivel de cada entrevistador.

    Sendo assim, com o join da forma que eu estava tentando

    session.createCriteria(EntrevistadorPesquisa.class)
    .createCriteria(“entrevistador”)
    .createCriteria(“usuario”);
    .createCriteria(“nivelEntrevistador”)

    eu teria meus dados não mão, correto?
    NÃO, ERRADO.

    Como vc disse, por padrão o hibernate não deixa eu fazer isso, pois consome muita memória, por muitas vezes atoa.

    Pois então é preciso mudar a propriedade @ManyToOne da tabela Entrevistador, de LAZY para EAGER.

    Sendo assim, só mudei minha configuração:
    – @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = “ID_USUARIO”, nullable = false)
    public Usuario getUsuario() {
    return this.usuario;
    }

    – @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = “ID_NIVEL”, nullable = false)
    public NivelEntrevistador getNivelEntrevistador() {
    return this.nivelEntrevistador;
    }

    e obtive sucesso na minha consulta.

    O que achou? O problema era semelhante mesmo? Espero que tenha sido útil a resposta
    Aguardo uma resposta por email, pra gente trocar idéia. Obrigado e até mais.

  2. ggarnier says:

    Ânderson, se eu entendi bem, acho que não é o mesmo caso. No exemplo que eu citei, eu tinha 2 tabelas e precisava fazer 2 joins entre elas. No seu caso são várias tabelas diferentes, e isso o Hibernate suporta bem.

    Só uma observação: o FetchType.EAGER não deve ser usado em relacionamentos OneToMany, pois cada vez que você buscar um registro, buscará também uma lista de elementos da outra tabela, o que pode provocar um impacto considerável no desempenho da sua aplicação.

  3. Ânderson Soares says:

    Sim. Nesse caso, o EAGER soloucionou bem meu caso, ou existe alguma forma melhor?

  4. ggarnier says:

    No seu caso não há problema, pois o EAGER foi colocado no @ManyToOne. O problema é quando ele é usado no @OneToMany, ou seja, na outra ponta do relacionamento, onde o EAGER faria com que um select buscasse também uma lista de objetos da outra entidade.

  5. Zenas says:

    Olá amigo,

    Aqui onde trabalho, a gente utiliza dessa forma e funciona;

    Criteria criteria = getSession().createCriteria( ProdutoProjetoVO.class, “produtoProjeto” );
    criteria.createAlias(“produtoProjeto.projetoTipoProduto.projeto”, “p_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.projetoTipoProduto.tipoProduto”, “tP_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.projetoTipoProduto.tipoProduto.statusProduto”, “tpS_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.projetoTipoProduto”, “pTP_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.statusProduto”, “sP_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.faseProjeto”, “fP_”, JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.projetoTipoProduto.projeto.empreendimento”, “e_”,JoinFragment.INNER_JOIN);
    criteria.createAlias(“produtoProjeto.projetoTipoProduto.projeto.empreendimento.areaGerencial”, “a_”,JoinFragment.INNER_JOIN);

    ProjectionList pList = getDefaultProjectionList()
    .add(Projections.property( “e_.id” ).as( “projetoTipoProduto.projeto.empreendimento.id” ) )
    .add(Projections.property( “e_.sigla” ).as( “projetoTipoProduto.projeto.empreendimento.sigla” ) )
    .add(Projections.property( “e_.nome” ).as( “projetoTipoProduto.projeto.empreendimento.nome” ) )
    .add(Projections.property( “a_.id” ).as( “projetoTipoProduto.projeto.empreendimento.areaGerencial.id” ) )
    .add(Projections.property( “a_.nome” ).as( “projetoTipoProduto.projeto.empreendimento.areaGerencial.nome” ) )
    .add(Projections.property(“tpS_.id”).as(“projetoTipoProduto.tipoProduto.statusProduto.id”))
    .add(Projections.property(“tpS_.descricao”).as(“projetoTipoProduto.tipoProduto.statusProduto.descricao”))
    .add(Projections.property( “p_.tipoProjeto” ).as(“projetoTipoProduto.projeto.tipoProjeto”));
    criteria.setProjection(pList);

    criteria.add( Restrictions.ne( “sP_.id”, StatusProdutoVO.EXPIRADO ) );

    criteria.add( Restrictions.or( Restrictions.eq( “tP_.id”, TipoProdutoVO.HISTOGRAMA_PLANEJADO ),
    Restrictions.eq( “tP_.id”, TipoProdutoVO.HISTOGRAMA_REALIZADO ) ) );

    criteria.add( Restrictions.eq( “fP_.id”, FaseProjetoVO.FASE_4 ) );
    criteria.add( Restrictions.eq( “p_.tipoProjeto”, ProjetoVO.TIPO_PROJETO ) );

    if ( empreendimento != null && empreendimento.getAreaGerencial() != null &&
    empreendimento.getAreaGerencial().getId() != null && empreendimento.getAreaGerencial().getId() > 0 ){
    criteria.add( Restrictions.eq( “a_.id”, empreendimento.getAreaGerencial().getId() ) );

    }

    if ( idProjeto != null && idProjeto > 0 ){
    criteria.add( Restrictions.eq( “p_.id”, idProjeto ) );

    }
    if ( empreendimento != null && empreendimento.getId() != null && empreendimento.getId() > 0 ){
    criteria.add( Restrictions.eq( “e_.id”, empreendimento.getId() ) );

    }

    criteria.addOrder( Order.asc( “e_.nome” ) );
    criteria.addOrder( Order.asc( “p_.nome” ) );
    criteria.addOrder( Order.asc( “tP_.nome” ) );
    criteria.addOrder( Order.desc( “produtoProjeto.versao” ) );

    HibernateTransformerVO transformer = new HibernateTransformerVO(ProdutoProjetoVO.class);
    criteria.setResultTransformer(transformer);

    return (Collection) criteria.list();

    Abraços

  6. Se eu entendi bem o seu código, não é o mesmo caso. No seu exemplo, você procura por projetos com tipo = HISTOGRAMA_PLANEJADO ou HISTOGRAMA_REALIZADO, certo?

    No exemplo que citei no post, eu procurava por projetos com histórico iniciado ou finalizado, mas precisava definir valores diferentes de data para cada caso (se for histórico iniciado, a data deve ser entre dataIniciadoDe e dataIniciadoAte; se for histórico finalizado, a data deve ser entre dataFinalizadoDe e dataFinalizadoAte).

Leave a Reply