A busca rápida de JPA não se associa

112

O que exatamente a estratégia de busca do JPA controla? Não consigo detectar nenhuma diferença entre ansioso e preguiçoso. Em ambos os casos JPA / Hibernate não associa automaticamente relacionamentos muitos para um.

Exemplo: a pessoa possui um único endereço. Um endereço pode pertencer a muitas pessoas. As classes de entidade anotadas JPA se parecem com:

@Entity
public class Person {
    @Id
    public Integer id;

    public String name;

    @ManyToOne(fetch=FetchType.LAZY or EAGER)
    public Address address;
}

@Entity
public class Address {
    @Id
    public Integer id;

    public String name;
}

Se eu usar a consulta JPA:

select p from Person p where ...

JPA / Hibernate gera uma consulta SQL para selecionar na tabela Person e, em seguida, uma consulta de endereço distinta para cada pessoa:

select ... from Person where ...
select ... from Address where id=1
select ... from Address where id=2
select ... from Address where id=3

Isso é muito ruim para grandes conjuntos de resultados. Se houver 1000 pessoas ele gera 1001 consultas (1 de Pessoa e 1000 distintas de Endereço). Eu sei disso porque estou olhando o log de consultas do MySQL. Eu entendi que definir o tipo de busca de endereço como ansioso fará com que o JPA / Hibernate consulte automaticamente com uma junção. No entanto, independentemente do tipo de busca, ele ainda gera consultas distintas para relacionamentos.

Somente quando eu digo explicitamente para entrar, ele realmente entra:

select p, a from Person p left join p.address a where ...

Estou faltando alguma coisa aqui? Agora tenho que codificar manualmente cada consulta para que ela se junte aos relacionamentos muitos para um. Estou usando a implementação JPA do Hibernate com MySQL.

Editar: Parece (consulte Hibernate FAQ aqui e aqui ) que FetchTypenão afeta as consultas JPA. Portanto, no meu caso, eu disse explicitamente para entrar.

Steve Kuo
fonte
5
Links para entradas de FAQ são quebradas, aqui está trabalhando um
n0weak

Respostas:

99

O JPA não fornece nenhuma especificação sobre anotações de mapeamento para selecionar a estratégia de busca. Em geral, as entidades relacionadas podem ser obtidas de qualquer uma das maneiras fornecidas abaixo

  • SELECT => uma consulta para entidades raiz + uma consulta para entidade mapeada / coleção relacionada de cada entidade raiz = (n + 1) consultas
  • SUBSELECT => uma consulta para entidades raiz + segunda consulta para entidade mapeada / coleção de todas as entidades raiz recuperadas na primeira consulta = 2 consultas
  • JOIN => uma consulta para buscar as entidades raiz e todas as suas entidades / coleções mapeadas = 1 consulta

Assim SELECTe JOINsão dois extremos e SUBSELECTcaem no meio. Pode-se escolher a estratégia adequada com base em seu modelo de domínio.

Por padrão, SELECTé usado por JPA / EclipseLink e Hibernate. Isso pode ser substituído usando:

@Fetch(FetchMode.JOIN) 
@Fetch(FetchMode.SUBSELECT)

em Hibernate. Ele também permite definir o SELECTmodo explicitamente usando o @Fetch(FetchMode.SELECT)que pode ser ajustado usando o tamanho do lote, por exemplo @BatchSize(size=10).

As anotações correspondentes no EclipseLink são:

@JoinFetch
@BatchFetch
Yadu
fonte
5
Por que essas configurações existem? Eu acho que JOIN deve ser usado quase sempre. Agora preciso marcar todos os mapeamentos com anotações específicas do hibernate.
vbezhenar
4
Interessante, mas infelizmente @Fetch (FetchMode.JOIN) não funciona para mim (Hibernate 4.2.15), em JPQL como em Criteria.
Aphax de
3
A anotação do Hibernate parece não funcionar para mim também, usando Spring JPA
TheBakker
2
@Aphax Isso pode ser porque o Hibernate usa diferentes estratégias padrão para JPAQL / Criteria vs em.find (). Consulte vladmihalcea.com/2013/10/17/… e a documentação de referência.
Joshua Davis
1
@vbezhenar (e outros lendo seu comentário algum tempo depois): A consulta JOIN gera o produto cartesiano no banco de dados, então você deve ter certeza de que deseja que o produto cartesiano seja calculado. Observe que se você usar o fetch join, mesmo se você colocar o LAZY, ele será carregado rapidamente.
Walfrat
45

"mxc" está certo. fetchTypeapenas especifica quando a relação deve ser resolvida.

Para otimizar o carregamento antecipado usando uma junção externa, você deve adicionar

@Fetch(FetchMode.JOIN)

para o seu campo. Esta é uma anotação específica de hibernação.

Rudolfson
fonte
6
Isso não funciona para mim com Hibernate 4.2.15, em JPQL ou Criteria.
Aphax de
6
@Aphax Acho que é porque o JPAQL e os critérios não obedecem à especificação Fetch. A anotação Fetch funciona apenas para em.find (), AFAIK. Consulte vladmihalcea.com/2013/10/17/… Além disso, consulte os documentos de hibernação. Tenho certeza que isso está coberto em algum lugar.
Joshua Davis
@JoshuaDavis O que quero dizer é que a anotação \ @Fetch não aplica nenhum tipo de otimização JOIN nas consultas, seja JPQL ou em.find (), acabei de experimentar o Hibernate 5.2. + E continua o mesmo
Aphax 01 de
38

O atributo fetchType controla se o campo anotado é buscado imediatamente quando a entidade primária é buscada. Isso não determina necessariamente como a instrução fetch é construída, a implementação real do sql depende do provedor que você está usando toplink / hibernate etc.

Se você definir fetchType=EAGERIsso significa que o campo anotado é preenchido com seus valores ao mesmo tempo que os outros campos na entidade. Portanto, se você abrir um gerenciador de entidade, recupere seus objetos pessoais e, em seguida, feche o gerenciador de entidade, subsequentemente, fazer uma pessoa.address não resultará em uma exceção de carregamento lento.

Se você definir, fetchType=LAZYo campo só será preenchido quando for acessado. Se você tiver fechado o entitymanager até então, uma exceção de carregamento lento será lançada se você fizer um person.address. Para carregar o campo, você precisa colocar a entidade de volta em um contexto de gerenciadores de entidades com em.merge (), acessar o campo e fechar o gerenciador de entidades.

Você pode desejar o carregamento lento ao construir uma classe de cliente com uma coleção para pedidos de cliente. Se você recuperou todos os pedidos de um cliente quando queria obter uma lista de clientes, esta pode ser uma operação de banco de dados cara quando você procura apenas o nome do cliente e detalhes de contato. Melhor deixar o acesso db para mais tarde.

Para a segunda parte da pergunta - como hibernar para gerar SQL otimizado?

O Hibernate deve permitir que você forneça dicas de como construir a consulta mais eficiente, mas suspeito que haja algo errado com a construção da sua tabela. A relação é estabelecida nas tabelas? O Hibernate pode ter decidido que uma consulta simples será mais rápida do que uma junção, especialmente se os índices etc. estiverem ausentes.

mxc
fonte
20

Experimente com:

select p from Person p left join FETCH p.address a where...

Ele funciona para mim de maneira semelhante ao JPA2 / EclipseLink, mas parece que esse recurso também está presente no JPA1 :

sinuhepop
fonte
7

Se você usar o EclipseLink em vez do Hibernate, você pode otimizar suas consultas por "dicas de consulta". Consulte este artigo do Eclipse Wiki: EclipseLink / Examples / JPA / QueryOptimization .

Há um capítulo sobre "Leitura conjunta".

uı6ʎɹnɯ ꞁəıuɐp
fonte
2

para participar, você pode fazer várias coisas (usando eclipselink)

  • em jpql você pode fazer busca de junção à esquerda

  • na consulta nomeada, você pode especificar a dica da consulta

  • em TypedQuery você pode dizer algo como

    query.setHint("eclipselink.join-fetch", "e.projects.milestones");

  • há também uma dica de busca em lote

    query.setHint("eclipselink.batch", "e.address");

Vejo

http://java-persistence-performance.blogspot.com/2010/08/batch-fetching-optimizing-object-graph.html

Kalpesh Soni
fonte
1

Eu tive exatamente esse problema com a exceção de que a classe Person tinha uma classe de chave incorporada. Minha própria solução foi juntá-los na consulta E remover

@Fetch(FetchMode.JOIN)

Minha classe de id incorporada:

@Embeddable
public class MessageRecipientId implements Serializable {

    @ManyToOne(targetEntity = Message.class, fetch = FetchType.LAZY)
    @JoinColumn(name="messageId")
    private Message message;
    private String governmentId;

    public MessageRecipientId() {
    }

    public Message getMessage() {
        return message;
    }

    public void setMessage(Message message) {
        this.message = message;
    }

    public String getGovernmentId() {
        return governmentId;
    }

    public void setGovernmentId(String governmentId) {
        this.governmentId = governmentId;
    }

    public MessageRecipientId(Message message, GovernmentId governmentId) {
        this.message = message;
        this.governmentId = governmentId.getValue();
    }

}
Pistoleiro
fonte
-1

Duas coisas me ocorrem.

Primeiro, você tem certeza de que quer dizer ManyToOne para endereço? Isso significa que várias pessoas terão o mesmo endereço. Se for editado para um deles, será editado para todos eles. É essa a sua intenção? 99% das vezes os endereços são "privados" (no sentido de que pertencem a apenas uma pessoa).

Em segundo lugar, você tem algum outro relacionamento ativo na entidade Person? Se bem me lembro, o Hibernate só pode lidar com um relacionamento ansioso em uma entidade, mas essa informação possivelmente está desatualizada.

Digo isso porque sua compreensão de como isso deve funcionar está essencialmente correta de onde estou sentado.

cletus
fonte
Este é um exemplo inventado que usa muitos para um. O endereço pessoal pode não ter sido o melhor exemplo. Não vejo nenhum outro tipo de busca rápida em meu código.
Steve Kuo de
Minha sugestão, então, é reduzir isso a um exemplo simples que executa e faz o que você está vendo e depois publique isso. Pode haver outras complicações em seu modelo que estão causando um comportamento inesperado.
cletus de
1
Executei o código exatamente como aparece acima e ele exibe o referido comportamento.
Steve Kuo de