JPA наследование @EntityGraph включает необязательные ассоциации подклассов - PullRequest
13 голосов
/ 16 апреля 2020

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

enter image description here

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

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

Использование этого

@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.

Ответы [ 4 ]

8 голосов
/ 29 апреля 2020

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

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

1-й запрос выбирает 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();

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

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

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

7 голосов
/ 17 апреля 2020

Я не знаю, что там делает 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();
}

Благодаря интеграции данных пружины, предоставляемой 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
0 голосов
/ 25 апреля 2020

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

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

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

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

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

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

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

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

  • Секунда для указанного значения c 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;
// ..
}
0 голосов
/ 22 апреля 2020

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

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

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

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

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

...