JPA наследование @EntityGraph включает необязательные ассоциации подклассов


12

Учитывая следующую модель предметной области, я хочу загрузить все Answers, включая их Valueи соответствующие дочерние элементы, и поместить их в, AnswerDTOчтобы затем преобразовать в JSON. У меня есть рабочее решение, но оно страдает от проблемы N + 1, от которой я хочу избавиться, используя специальное решение @EntityGraph. Все ассоциации настроены LAZY.

введите описание изображения здесь

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

Использование Однорангового @EntityGraphна Repositoryметоде , который я могу гарантировать , что значения предварительно натянутые предотвратить N +-на Answer->Valueассоциации. Хотя мой результат в порядке, есть еще одна проблема N + 1, из-за ленивой загрузки selectedассоциации MCValues.

Используя это

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

терпит неудачу, потому что selectedполе, конечно, является только частью некоторых Valueобъектов:

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

Как я могу сказать JPA только попытаться получить selectedассоциацию, если значение равно MCValue? Мне нужно что - то вроде optionalAttributePaths.

Ответы:


8

Вы можете использовать только, EntityGraphесли атрибут ассоциации является частью суперкласса и тем самым также частью всех подклассов. В противном случае, EntityGraphвсегда будет сбой с тем, Exceptionчто вы в настоящее время получаете.

Лучший способ избежать проблемы выбора N + 1 - разделить ваш запрос на 2 запроса:

Первый запрос выбирает MCValueсущности, используя EntityGraphдля извлечения ассоциации, сопоставленной selectedатрибутом. После этого запроса эти объекты затем сохраняются в кэше 1-го уровня Hibernate / в контексте постоянства. Hibernate будет использовать их при обработке результата 2-го запроса.

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

Затем второй запрос извлекает Answerсущность и использует EntityGraphтакже для извлечения связанных Valueсущностей. Для каждого Valueобъекта Hibernate создаст конкретный подкласс и проверит, содержит ли кэш 1-го уровня объект для этого класса и комбинацию первичного ключа. В этом случае Hibernate использует объект из кэша 1-го уровня вместо данных, возвращаемых запросом.

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

Поскольку мы уже получили все MCValueобъекты со связанными selectedобъектами, теперь мы получаем Answerобъекты с инициализированной valueассоциацией. И если ассоциация содержит MCValueобъект, его selectedассоциация также будет инициализирована.


Я думал о двух запросах: первый для получения ответов + значение и второй для получения selectedтех ответов, которые имеют MCValue. Мне не понравилось, что для этого потребуется дополнительный цикл, и мне нужно будет управлять отображением между наборами данных. Мне нравится ваша идея использовать кеш Hibernate для этого. Можете ли вы уточнить, насколько безопасно (с точки зрения согласованности) полагаться на кэш для хранения результатов? Это работает, когда запросы сделаны в транзакции? Я боюсь трудно заметить и случайные ленивые ошибки инициализации.
Застрял

1
Вам необходимо выполнить оба запроса в рамках одной транзакции. Пока вы делаете это и не очищаете свой постоянный контекст, это абсолютно безопасно. Ваш кэш 1-го уровня всегда будет содержать MCValueсущности. И вам не нужен дополнительный цикл. Вы должны выбрать все MCValueобъекты с Answerодним запросом, который присоединяется к и использует то же предложение WHERE, что и текущий запрос. Я также говорил об этом в сегодняшнем прямом эфире: youtu.be/70B9znTmi00?t=238 Это началось в 3:58, но я взял несколько других вопросов между ними ...
Торбен Янссен

Отлично, спасибо за продолжение! Также я хочу добавить, что для этого решения требуется 1 запрос на подкласс. Так что ремонтопригодность нам подходит, но это решение может не подходить для всех случаев.
Застрял

Мне нужно немного исправить свой последний комментарий: Конечно, вам нужен только запрос для подкласса, который страдает от проблемы. Также стоит отметить, что для атрибутов подклассов это, похоже, не является проблемой из-за использования SINGLE_TABLE_INHERITANCE.
Застрял

7

Я не знаю, что там делает Spring-Data, но для этого обычно нужно использовать TREATоператор, чтобы получить доступ к подассоциации, но реализация для этого оператора довольно глючная. Hibernate поддерживает неявный доступ к свойствам подтипов, который вам здесь нужен, но, очевидно, Spring-Data не может справиться с этим должным образом. Я могу порекомендовать вам взглянуть на Blaze-Persistence Entity-Views , библиотеку, которая работает поверх JPA и позволяет сопоставлять произвольные структуры с вашей моделью сущностей. Вы можете отобразить свою модель DTO безопасным способом, а также структуру наследования. Представления сущностей для вашего варианта использования могут выглядеть следующим образом

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

Благодаря интеграции данных Spring, предоставляемой Blaze-Persistence, вы можете определить репозиторий, подобный этому, и напрямую использовать результат

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

Он сгенерирует HQL-запрос, который выбирает только то, что вы отобразили в AnswerDTOчто-то вроде следующего.

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

Хм, спасибо за подсказку для вашей библиотеки, которую я уже нашел, но мы не будем использовать ее по двум основным причинам: 1) мы не можем полагаться на то, что библиотека будет поддерживаться в течение всего жизненного цикла нашего проекта (ваша компания blazebit довольно мала и в его начале). 2) Мы бы не стали использовать более сложный технологический стек для оптимизации одного запроса. (Я знаю, что ваша библиотека может сделать больше, но мы предпочитаем общий технический стек и скорее просто реализуем пользовательский запрос / преобразование, если нет решения JPA).
Застрял

1
Blaze-Persistence с открытым исходным кодом, а Entity-Views более или менее реализованы поверх стандартного JPQL / HQL. Реализуемые ею функции стабильны и будут работать с будущими версиями Hibernate, потому что он работает поверх стандарта. Я понимаю, что вы не хотите вводить что-то из-за одного варианта использования, но я сомневаюсь, что это единственный вариант использования, для которого вы могли бы использовать Entity Views. Представление Entity Views обычно приводит к значительному сокращению объема стандартного кода, а также повышает производительность запросов. Если вы не хотите использовать инструменты, которые вам помогут, пусть будет так.
Кристиан Бейков

По крайней мере, вы решили проблему и предоставили решение. Таким образом, вы получаете вознаграждение, хотя ответы не объясняют, что именно происходит в исходной проблеме и как JPA может ее решить. По моему мнению, он просто не поддерживается JPA и должен стать запросом на добавление функций. Я предложу еще одну награду за более подробный ответ, ориентированный только на JPA.
Застрял

Это просто невозможно с JPA. Вам нужен оператор TREAT, который не полностью поддерживается ни в одном JPA-провайдере и не поддерживается в аннотациях EntityGraph. Таким образом, единственный способ, которым вы можете смоделировать это, - это функция разрешения неявных свойств подтипа Hibernate, которая требует использования явных объединений.
Кристиан Бейков

1
В вашем ответе определение вида должно бытьinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Застрял

0

Мой последний проект использовал GraphQL (первый для меня), и у нас была большая проблема с N + 1 запросами и попыткой оптимизировать запросы так, чтобы они объединялись только для таблиц, когда они необходимы. Я нашел Cosium / spring-data-jpa-entity-graph незаменимым. Он расширяет JpaRepositoryи добавляет методы для передачи графа сущностей в запрос. Затем вы можете строить динамические графы сущностей во время выполнения, чтобы добавлять в левые объединения только те данные, которые вам нужны.

Наш поток данных выглядит примерно так:

  1. Получить запрос GraphQL
  2. Разбор запроса GraphQL и преобразование в список узлов графа сущностей в запросе
  3. Создайте граф сущностей из обнаруженных узлов и передайте в хранилище для выполнения

Чтобы решить проблему не включения недопустимых узлов в граф сущностей (например, __typenameиз graphql), я создал служебный класс, который обрабатывает генерацию графа сущностей. Вызывающий класс передает имя класса, для которого он генерирует граф, который затем проверяет каждый узел в графе на соответствие метамодели, поддерживаемой ORM. Если узел отсутствует в модели, он удаляет его из списка узлов графа. (Эта проверка должна быть рекурсивной и проверять каждого ребенка)

Прежде чем найти это, я попробовал проекции и любые другие альтернативы, рекомендованные в документах Spring JPA / Hibernate, но, похоже, ничто не решило проблему элегантно или, по крайней мере, с помощью тонны дополнительного кода.


как это решает проблему загрузки ассоциаций, которые не известны из супер типа? Кроме того, как сказано в другом ответе, мы хотим знать, существует ли чисто JPA-решение, но я также думаю, что lib страдает той же проблемой, что selectedассоциация доступна не для всех подтипов value.
Застрял

Если вы заинтересованы в GraphQL, у нас также есть интеграция Blaze-Persistence Entity Views с graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Кристиан Бейков

@ChristianBeikov спасибо, но мы используем SQPR для программной генерации нашей схемы из наших моделей / методов
aarbor

Если вам нравится подход, основанный на коде, вам понравится интеграция с GraphQL. Он обрабатывает выборку только фактически используемых столбцов / выражений, сокращая объединения и т. Д. Автоматически.
Кристиан Бейков

0

Отредактировано после вашего комментария:

Приношу свои извинения, я не справился с вашей проблемой в первом раунде, ваша проблема возникает при запуске данных Spring, а не только при попытке вызвать findAll ().

Итак, теперь вы можете перемещаться по полному примеру, который можно взять из моего github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/.

Вы можете легко воспроизвести и исправить вашу проблему в этом проекте.

В действительности, данные Spring и спящий режим не способны определять «выбранный» график по умолчанию, и вам необходимо указать способ сбора выбранного параметра.

Итак, сначала вы должны объявить NamedEntityGraphs класса Ответ

Как видите, есть два NamedEntityGraph для значения атрибута класса Answer

  • Первое для всех значение без конкретного отношения к загрузке

  • Второй для определенного значения Multichoice . Если вы удалите это, вы воспроизведете исключение.

Во-вторых, вам нужно быть в транзакционном контексте answerRepository.findAll (), если вы хотите получать данные типа 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;
// ..
}

Проблема не в получении value-ассоциации, Answerа в получении selectedассоциации в случае, если valueесть MCValue. Ваш ответ не содержит никакой информации об этом.
Застрял

@Stuck Спасибо за ваш ответ, не могли бы вы поделиться со мной классом MCValue, я постараюсь воспроизвести вашу проблему на месте.
bdzzaid

Ваш пример работает только потому, что вы определили ассоциацию OneToManyкак, FetchType.EAGERно как указано в вопросе: все ассоциации есть LAZY.
Застрял

@Stuck Я обновил свой ответ с момента вашего последнего обновления, надеюсь, вы знаете, что мой ответ поможет вам решить вашу проблему и поможет понять способ загрузки графа сущностей, включая дополнительные отношения.
Bdzzaid

Ваше «решение» все еще страдает от исходной проблемы N + 1, о которой идет речь в этом вопросе: вставьте методы insert и find в различные транзакции вашего теста, и вы увидите, что jpa будет выдавать запрос к БД selectedдля каждого ответа вместо их предварительной загрузки.
Застрял
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.