Herança JPA @EntityGraph inclui associações opcionais de subclasses

12

Dado o modelo de domínio a seguir, quero carregar todos os Answers, incluindo seus Values e seus respectivos sub-filhos, e colocá-los em um AnswerDTOpara depois converter para JSON. Eu tenho uma solução funcional, mas ela sofre com o problema N + 1 do qual quero me livrar usando um ad-hoc @EntityGraph. Todas as associações estão configuradas LAZY.

insira a descrição da imagem aqui

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Usando um ad-hoc @EntityGraphno Repositorymétodo, posso garantir que os valores sejam pré-buscados para impedir N + 1 na Answer->Valueassociação. Enquanto meu resultado é bom, há outro problema N + 1, devido ao carregamento lento da selectedassociação dos MCValues.

Usando isto

@EntityGraph(attributePaths = {"value.selected"})

falha, porque o selectedcampo é obviamente apenas parte de algumas das Valueentidades:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

Como posso dizer à JPA que apenas tente buscar a selectedassociação caso o valor seja a MCValue? Eu preciso de algo parecido optionalAttributePaths.

Preso
fonte

Respostas:

8

Você só pode usar um EntityGraphse o atributo de associação fizer parte da superclasse e por isso também fizer parte de todas as subclasses. Caso contrário, EntityGraphsempre falhará com o Exceptionque você recebe atualmente.

A melhor maneira de evitar o problema de seleção do N + 1 é dividir sua consulta em duas consultas:

A primeira consulta busca as MCValueentidades usando um EntityGraphpara buscar a associação mapeada pelo selectedatributo. Após essa consulta, essas entidades são armazenadas no cache do 1º nível do Hibernate / no contexto de persistência. O Hibernate os usará quando processar o resultado da 2ª consulta.

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

A 2ª consulta busca a Answerentidade e usa um EntityGraphpara também buscar as Valueentidades associadas . Para cada Valueentidade, o Hibernate instanciará a subclasse específica e verificará se o cache do 1º nível já contém um objeto para essa combinação de classe e chave primária. Se for esse o caso, o Hibernate usa o objeto do cache do 1º nível em vez dos dados retornados pela consulta.

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Como já buscamos todas as MCValueentidades com as selectedentidades associadas , agora obtemos Answerentidades com uma valueassociação inicializada . E se a associação contiver uma MCValueentidade, sua selectedassociação também será inicializada.

Thorben Janssen
fonte
Pensei em ter duas consultas, a primeira para buscar respostas + valor e a segunda para buscar selectedas respostas que possuem a MCValue. Não gostava que isso exigisse um loop adicional e precisaria gerenciar o mapeamento entre os conjuntos de dados. Gosto da sua ideia de explorar o cache do Hibernate para isso. Você pode explicar como é seguro (em termos de consistência) confiar no cache para conter os resultados? Isso funciona quando as consultas são feitas em uma transação? Eu tenho medo de erros de inicialização preguiçosos difíceis de detectar e esporádicos.
Preso
11
Você precisa executar as duas consultas na mesma transação. Contanto que você faça isso e não limpe seu contexto de persistência, é absolutamente seguro. Seu cache de 1º nível sempre conterá as MCValueentidades. E você não precisa de um loop adicional. Você deve buscar todas as MCValueentidades com 1 consulta que se associe à Answere use a mesma cláusula WHERE da sua consulta atual. Também falei sobre isso na transmissão ao vivo de hoje: youtu.be/70B9znTmi00?t=238 Começou às 3:58, mas fiz algumas outras perguntas entre ...
Thorben Janssen
Ótimo, obrigado pelo acompanhamento! Também quero acrescentar que esta solução requer 1 consulta por subclasse. Portanto, a manutenção é boa para nós, mas essa solução pode não ser adequada para todos os casos.
Preso
Preciso corrigir um pouco meu último comentário: é claro que você só precisa de uma consulta por subclasse que sofra do problema. Também vale a pena notar que, para atributos das subclasses, isso parece não ser um problema, por causa do uso SINGLE_TABLE_INHERITANCE.
Preso
7

Não sei o que o Spring-Data está fazendo lá, mas para fazer isso, você geralmente precisa usar o TREAToperador para poder acessar a sub-associação, mas a implementação para esse Operador é bastante complicada. O Hibernate suporta o acesso implícito à propriedade de subtipos, o que você precisaria aqui, mas aparentemente o Spring-Data não pode lidar com isso corretamente. Eu posso recomendar que você dê uma olhada no Blaze-Persistence Entity-Views , uma biblioteca que funciona sobre a JPA que permite mapear estruturas arbitrárias em relação ao seu modelo de entidade. Você pode mapear seu modelo de DTO de uma maneira segura, além da estrutura de herança. As visualizações de entidade para o seu caso de uso podem ter esta aparência

@EntityView(Answer.class)
interface AnswerDTO {
  @IdMapping
  Long getId();
  ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
  @IdMapping
  Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
  String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
  int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
  @Mapping("selected.id")
  Set<Long> getOption();
}

Com a integração de dados do Spring fornecida pelo Blaze-Persistence, você pode definir um repositório como este e usar diretamente o resultado

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

Ele irá gerar uma consulta HQL que seleciona exatamente o que você mapeou no AnswerDTOqual é algo como o seguinte.

SELECT
  a.id, 
  v.id,
  TYPE(v), 
  CASE WHEN TYPE(v) = TextValue THEN v.text END,
  CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
  CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
Christian Beikov
fonte
Hmm, obrigado pela dica da sua biblioteca que eu já encontrei, mas não a usaríamos por 2 razões principais: 1) não podemos confiar na lib a ser suportada durante a vida útil do nosso projeto (o blazebit da sua empresa é bastante pequeno e em seus inícios). 2) Não nos comprometemos com uma pilha tecnológica mais complexa para otimizar uma única consulta. (Eu sei que sua lib pode fazer mais, mas preferimos uma pilha de tecnologia comum e, em vez disso, implementamos uma consulta / transformação personalizada se não houver uma solução JPA).
Preso
11
O Blaze-Persistence é de código aberto e o Entity-Views é mais ou menos implementado no JPQL / HQL, que é padrão. Os recursos que ele implementa são estáveis ​​e ainda funcionarão com versões futuras do Hibernate, porque funcionam acima do padrão. Entendo que você não deseja apresentar algo por causa de um único caso de uso, mas duvido que esse seja o único caso de uso para o qual você poderia usar as Entity Views. A introdução de exibições de entidade geralmente leva à redução significativa da quantidade de código padrão e também aumenta o desempenho da consulta. Se você não quiser usar ferramentas que o ajudem, que assim seja.
Christian Beikov
Pelo menos você subestima o problema e fornece uma solução. Portanto, você recebe a recompensa, mesmo que as respostas não expliquem o que está acontecendo no problema original e como a JPA poderia resolvê-lo. Na minha percepção, ele não é suportado pelo JPA e deve se tornar uma solicitação de recurso. Oferecerei outra recompensa por uma resposta mais elaborada direcionada apenas ao JPA.
Preso
Simplesmente não é possível com o JPA. Você precisa do operador TREAT, que não é totalmente suportado em nenhum provedor de JPA, nem nas anotações do EntityGraph. Portanto, a única maneira de modelar isso é através do recurso de resolução de propriedade do subtipo implícito do Hibernate, que requer o uso de junções explícitas.
Christian Beikov 26/04
11
Na sua resposta, a definição da visualização deve serinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Preso
0

Meu projeto mais recente usou o GraphQL (o primeiro para mim) e tivemos um grande problema com as consultas N + 1 e tentando otimizar as consultas para ingressar em tabelas somente quando necessárias. Eu encontrei Cosium / spring-data-jpa-entity-graph insubstituível. Ele estende JpaRepositorye adiciona métodos para passar um gráfico de entidade para a consulta. Em seguida, você pode criar gráficos de entidade dinâmicos em tempo de execução para adicionar uniões esquerdas para apenas os dados necessários.

Nosso fluxo de dados é mais ou menos assim:

  1. Receber solicitação do GraphQL
  2. Analise a solicitação GraphQL e converta na lista de nós do gráfico de entidade na consulta
  3. Crie um gráfico de entidade a partir dos nós descobertos e passe para o repositório para execução

Para resolver o problema de não incluir nós inválidos no gráfico da entidade (por exemplo, __typenamedo graphql), criei uma classe de utilitário que lida com a geração do gráfico da entidade. A classe de chamada passa no nome da classe para a qual está gerando o gráfico, que valida cada nó no gráfico com relação ao metamodelo mantido pelo ORM. Se o nó não estiver no modelo, ele será removido da lista de nós do gráfico. (Essa verificação precisa ser recursiva e verificar cada criança também)

Antes de descobrir isso, tentei projeções e todas as alternativas recomendadas nos documentos do Spring JPA / Hibernate, mas nada parecia resolver o problema com elegância ou pelo menos com uma tonelada de código extra

aarbor
fonte
como ele resolve o problema de carregar associações que não são conhecidas do tipo super? Além disso, como dito na outra resposta, queremos saber se existe uma solução JPA pura, mas também acho que a lib sofre o mesmo problema que a selectedassociação não está disponível para todos os subtipos de value.
Preso
Se você está interessado no GraphQL, também temos uma integração do Blaze-Persistence Entity Views com graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/
Christian Beikov
@ChristianBeikov obrigado, mas estamos usando o SQPR para gerar nosso esquema programaticamente a partir de nossos modelos / métodos
aarbor 24/04
Se você gosta da abordagem de primeiro código, vai adorar a integração do GraphQL. Ele controla a busca apenas das colunas / expressões realmente usadas, reduzindo as junções etc. automaticamente.
Christian Beikov 25/04
0

Editado após o seu comentário:

Peço desculpas, não subestimei seu problema na primeira rodada, seu problema ocorre na inicialização dos dados da primavera, não apenas quando você tenta chamar o findAll ().

Portanto, agora você pode navegar pelo exemplo completo que pode ser obtido no meu github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Você pode facilmente reproduzir e corrigir seu problema neste projeto.

De fato, os dados do Spring e o hibernate não são capazes de determinar o gráfico "selecionado" por padrão e você precisa especificar a maneira de coletar a opção selecionada.

Então, primeiro, você deve declarar o NamedEntityGraphs da classe Answer

Como você pode ver, há dois NamedEntityGraph para o valor do atributo da classe Answer

  • O primeiro para todos os Value sem relação específica para carregar

  • O segundo para o valor específico de escolha múltipla . Se você remover este, você reproduzirá a exceção.

Segundo, você precisa estar em um contexto transacional answerRepository.findAll () se quiser buscar dados no tipo LAZY

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}
bdzzaid
fonte
O problema não é buscar a associação valuede, Answermas obter a selectedassociação caso valueseja a MCValue. Sua resposta não inclui nenhuma informação sobre isso.
Preso
@ Preso Obrigado pela sua resposta, você pode compartilhar comigo a classe MCValue? Tentarei reproduzir seu problema localmente.
bdzzaid em 25/04
Seu exemplo funciona apenas porque você definiu a associação OneToManycomo FetchType.EAGERmas como indicado na pergunta: todas as associações são LAZY.
Preso
@ Preso Atualizei minha resposta desde a sua última atualização, espero que saiba que minha resposta o ajudará a resolver seu problema e a entender como carregar um gráfico de entidade, incluindo relacionamentos opcionais.
bdzzaid 28/04
Sua "solução" ainda sofre com o problema N + 1 original de que trata esta questão: coloque os métodos insert e find em diferentes transações do seu teste e você verá que o jpa emitirá uma consulta de banco de dados selectedpara todas as respostas, em vez de carregá-las antecipadamente.
Preso