La herencia JPA @EntityGraph incluye asociaciones opcionales de subclases

12

Dado el siguiente modelo de dominio, quiero cargar todos los correos Answerelectrónicos incluidos sus Valuecorreos electrónicos y sus respectivos hijos secundarios y ponerlos en un AnswerDTOpara luego convertirlos a JSON. Tengo una solución que funciona pero sufre del problema N + 1 del que quiero deshacerme usando un ad-hoc @EntityGraph. Todas las asociaciones están configuradas LAZY.

ingrese la descripción de la imagen aquí

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

Usando un ad-hoc @EntityGraphen el Repositorymétodo, puedo asegurarme de que los valores se obtienen previamente para evitar N + 1 en la Answer->Valueasociación. Si bien mi resultado está bien, hay otro problema de N + 1, debido a la carga lenta de la selectedasociación del MCValues.

Usando esto

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

falla, porque el selectedcampo es, por supuesto, solo parte de algunas de las Valueentidades:

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

¿Cómo puedo decirle a JPA que solo intente buscar la selectedasociación en caso de que el valor sea a MCValue? Necesito algo así optionalAttributePaths.

Atascado
fuente

Respuestas:

8

Solo puede usar un EntityGraphsi el atributo de asociación es parte de la superclase y por eso también parte de todas las subclases. De lo contrario, EntityGraphsiempre fallará con lo Exceptionque actualmente tiene.

La mejor manera de evitar su problema de selección N + 1 es dividir su consulta en 2 consultas:

La primera consulta recupera las MCValueentidades usando un EntityGraphpara recuperar la asociación asignada por el selectedatributo. Después de esa consulta, estas entidades se almacenan en el caché de primer nivel de Hibernate / el contexto de persistencia. Hibernate los usará cuando procese el resultado de la segunda consulta.

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

La segunda consulta luego recupera la Answerentidad y usa un EntityGraphpara recuperar también las Valueentidades asociadas . Para cada Valueentidad, Hibernate instanciará la subclase específica y verificará si el caché de primer nivel ya contiene un objeto para esa combinación de clase y clave principal. Si ese es el caso, Hibernate usa el objeto del caché de primer nivel en lugar de los datos devueltos por la consulta.

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

Como ya obtuvimos todas las MCValueentidades con las selectedentidades asociadas , ahora obtenemos Answerentidades con una valueasociación inicializada . Y si la asociación contiene una MCValueentidad, su selectedasociación también se inicializará.

Thorben Janssen
fuente
Pensé en tener dos consultas, la primera para obtener respuestas + valor y una segunda para obtener selectedlas respuestas que tienen un MCValue. No me gustó que esto requeriría un bucle adicional y tendría que administrar la asignación entre los conjuntos de datos. Me gusta tu idea de explotar el caché de Hibernate para esto. ¿Puede explicar qué tan seguro (en términos de consistencia) es confiar en el caché para contener los resultados? ¿Funciona esto cuando las consultas se realizan en una transacción? Tengo miedo de los errores de inicialización diferidos esporádicos y difíciles de detectar.
Atrapado el
1
Debe realizar ambas consultas dentro de la misma transacción. Mientras lo haga y no borre su contexto de persistencia, es absolutamente seguro. Su caché de primer nivel siempre contendrá las MCValueentidades. Y no necesita un bucle adicional. Debe buscar todas las MCValueentidades con 1 consulta que se una a la Answery use la misma cláusula WHERE que su consulta actual. También hablé sobre esto en la transmisión en vivo de hoy: youtu.be/70B9znTmi00?t=238 Comenzó a las 3:58 pero tomé algunas otras preguntas en el medio ...
Thorben Janssen
Genial, gracias por el seguimiento! También quiero agregar que esta solución requiere 1 consulta por subclase. Por lo tanto, la mantenibilidad está bien para nosotros, pero esta solución podría no ser adecuada para todos los casos.
Atrapado el
Necesito corregir un poco mi último comentario: por supuesto, solo necesita una consulta por subclase que sufra el problema. También vale la pena señalar, que para los atributos de las subclases esto parece no ser un problema, debido al uso SINGLE_TABLE_INHERITANCE.
Atrapado el
7

No sé qué está haciendo Spring-Data allí, pero para hacer eso, generalmente debe usar el TREAToperador para poder acceder a la subasociación, pero la implementación para ese Operador es bastante defectuosa. Hibernate admite acceso de propiedad de subtipo implícito, que es lo que necesitaría aquí, pero aparentemente Spring-Data no puede manejar esto correctamente. Puedo recomendar que eche un vistazo a Blaze-Persistence Entity-Views , una biblioteca que funciona sobre JPA que le permite mapear estructuras arbitrarias contra su modelo de entidad. Puede asignar su modelo DTO de forma segura, también la estructura de herencia. Las vistas de entidad para su caso de uso podrían verse así

@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 la integración de datos de primavera proporcionada por Blaze-Persistence, puede definir un repositorio como este y usar directamente el resultado

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

Generará una consulta HQL que selecciona exactamente lo que asignó en el AnswerDTOcual es algo como lo siguiente.

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
fuente
Hmm, gracias por la sugerencia a su biblioteca que ya encontré, pero no la usaríamos por 2 razones principales: 1) no podemos confiar en que la biblioteca sea compatible durante la vida útil de nuestro proyecto (el blazebit de su empresa es bastante pequeño y en sus inicios). 2) No nos comprometeríamos con una pila tecnológica más compleja para optimizar una sola consulta. (Sé que su lib puede hacer más, pero preferimos una pila tecnológica común y más bien solo implementaríamos una consulta / transformación personalizada si no hay una solución JPA).
Atrapado el
1
Blaze-Persistence es de código abierto y Entity-Views se implementa más o menos sobre JPQL / HQL, que es estándar. Las características que implementa son estables y seguirán funcionando con futuras versiones de Hibernate, ya que funciona por encima del estándar. Entiendo que no desea introducir algo debido a un caso de uso único, pero dudo que sea el único caso de uso para el que podría usar Vistas de entidad. La introducción de Vistas de entidad generalmente conduce a reducir significativamente la cantidad de código repetitivo y también aumenta el rendimiento de la consulta. Si no desea utilizar herramientas que lo ayuden, que así sea.
Christian Beikov
Al menos no entendiste el problema y proporcionas una solución. Entonces obtienes la recompensa a pesar de que las respuestas no explican qué está sucediendo exactamente en el problema original y cómo JPA podría resolverlo. Desde mi punto de vista, JPA no lo admite y debería convertirse en una solicitud de función. Ofreceré otra recompensa por una respuesta más elaborada dirigida solo a JPA.
Atrapado el
Simplemente no es posible con JPA. Necesita el operador TREAT que no es totalmente compatible con ningún proveedor de JPA, ni es compatible con las anotaciones de EntityGraph. Entonces, la única forma de modelar esto es a través de la característica de resolución de propiedad de subtipo implícito Hibernate, que requiere que use combinaciones explícitas.
Christian Beikov
1
En su respuesta la definición de vista debería serinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Stuck
0

Mi último proyecto usó GraphQL (el primero para mí) y tuvimos un gran problema con las consultas N + 1 y tratando de optimizar las consultas para que solo se unan a las tablas cuando son necesarias. He encontrado que Cosium / spring-data-jpa-entity-graph es insustituible. Extiende JpaRepositoryy agrega métodos para pasar un gráfico de entidad a la consulta. Luego puede crear gráficos de entidades dinámicas en tiempo de ejecución para agregar uniones izquierdas solo para los datos que necesita.

Nuestro flujo de datos se parece a esto:

  1. Recibir solicitud de GraphQL
  2. Analice la solicitud de GraphQL y conviértala en una lista de nodos de gráfico de entidad en la consulta
  3. Cree un gráfico de entidad a partir de los nodos descubiertos y páselo al repositorio para su ejecución

Para resolver el problema de no incluir nodos no válidos en el gráfico de entidad (por ejemplo, __typenamedesde graphql), creé una clase de utilidad que maneja la generación del gráfico de entidad. La clase que llama pasa el nombre de la clase para la que genera el gráfico, que luego valida cada nodo en el gráfico contra el metamodelo mantenido por el ORM. Si el nodo no está en el modelo, lo elimina de la lista de nodos del gráfico. (Esta verificación debe ser recursiva y también verificar a cada niño)

Antes de encontrar esto, probé las proyecciones y cualquier otra alternativa recomendada en los documentos Spring JPA / Hibernate, pero nada parecía resolver el problema de manera elegante o al menos con un montón de código adicional.

aarbor
fuente
¿Cómo resuelve el problema de cargar asociaciones que no se conocen del tipo súper? Además, como se dijo en la otra respuesta, queremos saber si existe una solución JPA pura, pero también creo que la lib sufre el mismo problema que la selectedasociación no está disponible para todos los subtipos de value.
Atrapado el
Si está interesado en GraphQL, también tenemos una integración de Blaze-Persistence Entity Views con graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov
@ChristianBeikov gracias, pero estamos usando SQPR para generar nuestro esquema mediante programación a partir de nuestros modelos / métodos
aarbor
Si le gusta el enfoque de código primero, le encantará la integración GraphQL. Maneja la recuperación solo de las columnas / expresiones realmente utilizadas, lo que reduce las uniones, etc. de forma automática.
Christian Beikov
0

Editado después de tu comentario:

Pido disculpas, no he entendido bien su problema en la primera ronda, su problema ocurre al inicio de spring-data, no solo cuando intenta llamar a findAll ().

Entonces, ahora puede navegar, el ejemplo completo se puede extraer de mi github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Puede reproducir y solucionar su problema fácilmente dentro de este proyecto.

Efectivamente, los datos de Spring y la hibernación no son capaces de determinar el gráfico "seleccionado" de forma predeterminada y debe especificar la forma de recopilar la opción seleccionada.

Primero, debes declarar los NamedEntityGraphs de la clase Respuesta

Como puede ver, hay dos NamedEntityGraph para el valor de atributo de la clase Respuesta

  • El primero para todo Valor sin relación específica para cargar

  • El segundo para el valor específico de Multichoice . Si elimina este, reproduce la excepción.

En segundo lugar, debe estar en un contexto transaccional answerRepository.findAll () si desea obtener datos en 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
fuente
El problema no es buscar la asociación valuede Answersino obtener la selectedasociación en caso de que valuesea ​​a MCValue. Su respuesta no incluye ninguna información al respecto.
Atrapado el
@Stuck Gracias por tu respuesta, ¿puedes compartir conmigo la clase MCValue? Intentaré reproducir tu problema localmente.
bdzzaid
Su ejemplo solo funciona porque definió la asociación OneToManycomo FetchType.EAGERpero como se indica en la pregunta: todas las asociaciones son LAZY.
Atrapado el
@Stuck Actualicé mi respuesta desde su última actualización, espero que mi respuesta lo ayude a resolver su problema y lo ayude a comprender la forma de cargar un gráfico de entidad que incluye relaciones opcionales.
bdzzaid
Su "solución" todavía adolece del problema original de N + 1 sobre el que trata esta pregunta: ponga métodos de inserción y búsqueda en diferentes transacciones de su prueba y verá que jpa emitirá una consulta DB selectedpara cada respuesta en lugar de cargarlas por adelantado.
Atrapado el