L'ereditarietà JPA @EntityGraph include associazioni facoltative di sottoclassi


12

Dato il seguente modello di dominio, voglio caricare tutti Answeri messaggi inclusi Valuei loro figli secondari e inserirli in un file AnswerDTOper poi convertirli in JSON. Ho una soluzione funzionante ma soffre del problema N + 1 di cui voglio liberarmi usando un ad-hoc @EntityGraph. Tutte le associazioni sono configurate LAZY.

inserisci qui la descrizione dell'immagine

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

Utilizzando un metodo ad-hoc @EntityGraphsul Repositorymetodo, posso garantire che i valori siano pre-recuperati per impedire N + 1 sull'associazione Answer->Value. Mentre il mio risultato va bene c'è un altro problema N + 1, a causa del caricamento lento selecteddell'associazione della MCValues.

Usando questo

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

fallisce, perché il selectedcampo è ovviamente solo una parte di alcune Valueentità:

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

Come posso dire a JPA provare a recuperare l' selectedassociazione solo nel caso in cui il valore sia a MCValue? Ho bisogno di qualcosa del genere optionalAttributePaths.

Risposte:


8

È possibile utilizzare un solo EntityGraphse l'attributo di associazione fa parte della superclasse e anche da quella parte di tutte le sottoclassi. Altrimenti, EntityGraphfallirà sempre con quello Exceptionche si ottiene attualmente.

Il modo migliore per evitare il problema di selezione di N + 1 è dividere la query in 2 query:

La prima query recupera le MCValueentità utilizzando un EntityGraphper recuperare l'associazione mappata dall'attributo selected. Dopo quella query, queste entità vengono quindi archiviate nella cache di 1 ° livello di Hibernate / nel contesto di persistenza. Hibernate li userà quando elabora il risultato della seconda query.

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

La seconda query recupera quindi l' Answerentità e utilizza un EntityGraphper recuperare anche le Valueentità associate . Per ogni Valueentità, Hibernate crea un'istanza della sottoclasse specifica e verifica se la cache di 1 ° livello contiene già un oggetto per quella classe e la combinazione di chiave primaria. In tal caso, Hibernate utilizza l'oggetto dalla cache di 1 ° livello anziché i dati restituiti dalla query.

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

Poiché abbiamo già recuperato tutte le MCValueentità con le selectedentità associate , ora otteniamo Answerentità con un'associazione inizializzata value. E se l'associazione contiene MCValueun'entità, anche la sua selectedassociazione verrà inizializzata.


Ho pensato di avere due query, la prima per recuperare risposte + valore e una seconda per recuperare selectedquelle risposte che hanno un MCValue. Non mi è piaciuto che ciò richiederebbe un ciclo aggiuntivo e avrei bisogno di gestire il mapping tra i set di dati. Mi piace la tua idea di sfruttare la cache di Hibernate per questo. Puoi approfondire quanto è sicuro (in termini di coerenza) fare affidamento sulla cache per contenere i risultati? Funziona quando le query vengono fatte in una transazione? Ho paura di errori di inizializzazione pigri difficili da individuare e sporadici.
Bloccato il

1
È necessario eseguire entrambe le query all'interno della stessa transazione. Finché lo fai e non cancelli il tuo contesto di persistenza, è assolutamente sicuro. La cache di primo livello conterrà sempre le MCValueentità. E non hai bisogno di un loop aggiuntivo. È necessario recuperare tutte le MCValueentità con 1 query che si unisce alla Answere utilizza la stessa clausola WHERE della query corrente. Ne ho parlato anche nel live streaming di oggi: youtu.be/70B9znTmi00?t=238 È iniziato alle 3:58 ma ho preso alcune altre domande tra ...
Thorben Janssen

Ottimo, grazie per il seguito! Inoltre, voglio aggiungere che questa soluzione richiede 1 query per sottoclasse. Quindi la manutenibilità è ok per noi, ma questa soluzione potrebbe non essere adatta a tutti i casi.
Bloccato il

Devo correggere un po 'il mio ultimo commento: ovviamente hai solo bisogno di una query per sottoclasse che soffra del problema. Inoltre, vale la pena notare che per gli attributi delle sottoclassi questo non sembra essere un problema, a causa dell'uso SINGLE_TABLE_INHERITANCE.
Bloccato il

7

Non so cosa ci faccia Spring-Data, ma per farlo, di solito devi usare l' TREAToperatore per poter accedere alla sottoassociazione ma l'implementazione per quell'operatore è piuttosto errata. Hibernate supporta l'accesso implicito alla proprietà del sottotipo che è ciò di cui avresti bisogno qui, ma a quanto pare Spring-Data non può gestirlo correttamente. Posso consigliarti di dare un'occhiata a Blaze-Persistence Entity-Views , una libreria che funziona in cima a JPA che ti consente di mappare strutture arbitrarie sul tuo modello di entità. È possibile mappare il modello DTO in modo sicuro, anche la struttura ereditaria. Le viste delle entità per il tuo caso d'uso potrebbero apparire così

@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();
}

Con l'integrazione dei dati di primavera fornita da Blaze-Persistence puoi definire un repository come questo e utilizzare direttamente il risultato

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

Genererà una query HQL che seleziona esattamente ciò che è stato mappato nel AnswerDTOquale è qualcosa di simile al seguente.

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

Hmm grazie per il suggerimento per la tua libreria che ho già trovato, ma non lo useremmo per 2 motivi principali: 1) non possiamo fare affidamento sulla lib che sarà supportata per tutta la durata del nostro progetto (il tuo blazebit della tua azienda è piuttosto piccolo e nei suoi inizi). 2) Non ci impegneremmo in uno stack tecnologico più complesso per ottimizzare una singola query. (So ​​che la tua lib può fare di più, ma preferiamo uno stack tecnologico comune e piuttosto implementeremmo una query / trasformazione personalizzata se non ci fosse una soluzione JPA).
Bloccato il

1
Blaze-Persistence è open source ed Entity-Views è più o meno implementato su JPQL / HQL che è standard. Le funzionalità implementate sono stabili e continueranno a funzionare con le versioni future di Hibernate, perché funzionano al di sopra dello standard. Comprendo che non si desidera introdurre qualcosa a causa di un singolo caso d'uso, ma dubito che sia l'unico caso d'uso per il quale è possibile utilizzare Entity Views. L'introduzione di Entity Views di solito porta a ridurre in modo significativo la quantità di codice boilerplate e aumenta anche le prestazioni delle query. Se non vuoi usare strumenti che ti aiutano, così sia.
Christian Beikov,

Almeno hai capito male il problema e hai fornito una soluzione. Quindi ottieni la generosità anche se le risposte non spiegano cosa sta succedendo esattamente nel problema originale e come l'APP potrebbe risolverlo. Secondo la mia percezione, non è supportato da JPA e dovrebbe diventare una richiesta di funzionalità. Offrirò un'altra ricompensa per una risposta più elaborata rivolta esclusivamente all'APP.
Bloccato il

Semplicemente non è possibile con JPA. È necessario l'operatore TREAT che non è completamente supportato in nessun provider JPA, né è supportato nelle annotazioni EntityGraph. Pertanto, l'unico modo per modellarlo è tramite la funzione di risoluzione delle proprietà del sottotipo implicito di Hibernate, che richiede l'utilizzo di join espliciti.
Christian Beikov,

1
Nella tua risposta la definizione della vista dovrebbe essereinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Bloccata il

0

Il mio ultimo progetto ha utilizzato GraphQL (una novità per me) e abbiamo riscontrato un grosso problema con le query N + 1 e cercando di ottimizzare le query per unirle alle tabelle solo quando sono necessarie. Ho trovato Cosium / spring-data-jpa-entity-graph insostituibili. Estende JpaRepositorye aggiunge metodi per passare un grafico di entità alla query. È quindi possibile creare grafici di entità dinamici in fase di runtime per aggiungere join di sinistra solo per i dati necessari.

Il nostro flusso di dati è simile al seguente:

  1. Ricevi la richiesta GraphQL
  2. Analizzare la richiesta GraphQL e convertirla in un elenco di nodi del grafico dell'entità nella query
  3. Creare un grafico entità dai nodi rilevati e passare al repository per l'esecuzione

Per risolvere il problema di non includere nodi non validi nel grafico dell'entità (ad esempio __typenameda graphql), ho creato una classe di utilità che gestisce la generazione del grafico dell'entità. La classe chiamante passa nel nome della classe per cui sta generando il grafico, che quindi convalida ciascun nodo nel grafico rispetto al metamodello gestito dall'ORM. Se il nodo non è nel modello, lo rimuove dall'elenco dei nodi del grafico. (Questo controllo deve essere ricorsivo e controllare anche ogni bambino)

Prima di trovare questo avevo provato le proiezioni e ogni altra alternativa raccomandata nei documenti Spring JPA / Hibernate, ma nulla sembrava risolvere il problema elegantemente o almeno con un sacco di codice extra


come risolve il problema del caricamento di associazioni che non sono conosciute dal super tipo? Inoltre, come detto all'altra risposta, vogliamo sapere se esiste una soluzione JPA pura, ma penso anche che la lib soffra dello stesso problema che l' selectedassociazione non è disponibile per tutti i sottotipi di value.
Bloccato il

Se sei interessato a GraphQL, abbiamo anche un'integrazione di Blaze-Persistence Entity Views con graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov

@ChristianBeikov grazie ma stiamo usando SQPR per generare il nostro schema in modo programmatico dai nostri modelli / metodi
aarbor

Se ti piace l'approccio code-first, adorerai l'integrazione di GraphQL. Gestisce il recupero automatico solo delle colonne / espressioni effettivamente utilizzate, riducendo i join e così via.
Christian Beikov,

0

Modificato dopo il tuo commento:

Mi scuso, non ho sottovalutato il problema al primo turno, il problema si verifica all'avvio di spring-data, non solo quando si tenta di chiamare findAll ().

Quindi, ora puoi navigare l'esempio completo può essere estratto dal mio github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Puoi facilmente riprodurre e risolvere il problema all'interno di questo progetto.

In effetti, i dati di Spring e l'ibernazione non sono in grado di determinare il grafico "selezionato" per impostazione predefinita e è necessario specificare il modo in cui raccogliere l'opzione selezionata.

Quindi, prima, devi dichiarare i NamedEntityGraph della classe Answer

Come puoi vedere, ci sono due NamedEntityGraph per il valore dell'attributo della classe Answer

  • Il primo per tutti Valore senza relazione specifica da caricare

  • Il secondo per il valore Multichoice specifico . Se rimuovi questo, riproduci l'eccezione.

In secondo luogo, è necessario trovarsi in un contesto transazionale answerRepository.findAll () se si desidera recuperare i dati nel 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;
// ..
}

Il problema non è il recupero del value-Associazione dei Answer, ma ottenere selectedl'associazione nel caso in cui il valueè una MCValue. La tua risposta non include alcuna informazione in merito.
Bloccato il

@Stuck Grazie per la tua risposta, puoi per favore condividere con me la classe MCValue, proverò a riprodurre il problema localmente.
bdzzaid

Il tuo esempio funziona solo perché hai definito l'associazione OneToManycome FetchType.EAGERma come indicato nella domanda: tutte le associazioni lo sono LAZY.
Bloccato il

@Stuck Ho aggiornato la mia risposta dal tuo ultimo aggiornamento, spero che la mia risposta ti aiuti a risolvere il problema e ti aiuti a capire come caricare il grafico dell'entità, comprese le relazioni facoltative.
bdzzaid,

La tua "soluzione" soffre ancora del problema originale N + 1 di cui questa domanda riguarda: inserisci i metodi insert e find in diverse transazioni del tuo test e vedi che jpa invierà una query DB selectedper ogni risposta invece di caricarli in anticipo.
Bloccato il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.