Спадщина JPA @EntityGraph включає необов'язкові асоціації підкласів


12

З огляду на наступну модель домену, я хочу завантажити всі Answers, включаючи їх Values та їхніх піддітей та помістити його в, 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асоціацію, якщо значення є a MCValue? Мені потрібно щось на кшталт optionalAttributePaths.

Відповіді:


8

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

Найкращий спосіб уникнути проблеми вибору N + 1 - розділити ваш запит на 2 запити:

1-й запит отримує MCValueоб'єкти, що використовують a EntityGraphдля отримання асоціації, відображеної за selectedатрибутом. Після цього запиту ці об'єкти зберігаються в кеш-пам'яті першого рівня 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об'єкти з 1 запитом, який приєднується до Answerта використовує той самий пункт WHERE як ваш поточний запит. Про це я також говорив у поточному прямому ефірі: youtu.be/70B9znTmi00?t=238 Почалося о 3:58, але я взяв кілька інших запитань між ними ...
Thorben Janssen

Чудово, дякую за наступні дії! Також хочу додати, що для цього рішення потрібно 1 запит на підклас. Тож ремонтопридатність для нас нормальна, але це рішення може бути не підходящим у всіх випадках.
Застряв

Мені потрібно трохи виправити останній коментар: Звичайно, вам потрібен лише запит на підклас, який страждає від проблеми. Також варто відзначити, що для атрибутів підкласів це, здається, не викликає проблеми через використання SINGLE_TABLE_INHERITANCE.
Застряв

7

Я не знаю, що там роблять Spring-Data, але для цього зазвичай потрібно використовувати TREATоператор, щоб мати доступ до підключення, але реалізація для цього Оператора досить невміла. Hibernate підтримує неявний доступ до властивостей підтипу, який вам тут знадобиться, але, мабуть, Spring-Data не може справитись із цим належним чином. Я можу порекомендувати поглянути на Blaze-Persistent 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

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

1
Blaze-Persistence є відкритим кодом, і Entity-Views більш-менш реалізовано поверх JPQL / HQL, що є стандартним. Функції, які він реалізує, стабільні, і все ще працюватимуть з майбутніми версіями Hibernate, оскільки він працює вище стандартних. Я розумію, що ви не хочете щось вводити через єдиний випадок використання, але я сумніваюся, що це єдиний випадок використання, для якого ви могли використовувати 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-entit-graph не замінюється . Він розширює JpaRepositoryта додає методи для передачі графіка сутності до запиту. Потім ви можете створювати графіки динамічних об'єктів під час виконання, щоб додавати ліві з'єднання лише для потрібних даних.

Наш потік даних виглядає приблизно так:

  1. Отримати запит GraphQL
  2. Проаналізуйте запит GraphQL та перетворіть його до списку вузлів графіка сутності у запиті
  3. Створіть графік сутності з виявлених вузлів і перейдіть у сховище для виконання

Щоб вирішити проблему не включення недійсних вузлів до графіка сутності (наприклад, __typenameз graphql), я створив клас утиліти, який обробляє генерацію графіка сутності. Клас виклику передає ім'я класу, для якого він генерує графік, який потім перевіряє кожен вузол у графі проти метамоделі, що підтримується ORM. Якщо вузла немає в моделі, він видаляє його зі списку графічних вузлів. (Ця перевірка повинна бути рекурсивною і перевіряти кожну дитину)

Перш ніж знайти це, я спробував прогнози та будь-яку іншу альтернативу, рекомендовану у весняних документах JPA / Hibernate, але, здається, ніщо не вирішило проблему елегантно або, принаймні, з тоною додаткового коду


як це вирішує проблему завантаження асоціацій, невідомих із супертипу? Також, як сказано в іншій відповіді, ми хочемо знати, чи є чисте рішення JPA, але я також думаю, що lib страждає від тієї ж проблеми, що selectedасоціація доступна не для всіх підтипів value.
Застряг

Якщо вас цікавить GraphQL, ми також інтегруємо Blaze-Persistent Entity Views з graphql-java: persistent.blazebit.com/documentation/1.5/entity-view/manual/…
Крістіан Бейков

@ChristianBeikov спасибі, але ми використовуємо SQPR для програмного генерування нашої схеми з наших моделей / методів
aarbor

Якщо вам подобається перший підхід, вам сподобається інтеграція GraphQL. Він автоматично обробляє отримання фактично використаних стовпців / виразів, зменшуючи приєднання тощо.
Крістіан Бейков

0

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

Вибачте, я не підкреслив, що ви випускаєтесь у першому раунді, ваша проблема виникає при запуску весняних даних не тільки при спробі викликати findAll ().

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

Ви можете легко відтворити та виправити свою проблему всередині цього проекту.

Ефективно, дані Spring та сплячий режим не здатні визначати "вибраний" графік за замовчуванням, і вам потрібно вказати спосіб збирання вибраного параметра.

Отже, спочатку ви повинні оголосити NamedEntityGraphs класу Answer

Як ви можете бачити, є два NamedEntityGraph для атрибута значення цього класу Відповіді

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

  • Другий за конкретним значенням 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, що стосується цього питання: поставте вставку та знайдіть методи в різних транзакціях вашого тесту, і ви побачите, що jpa видасть запит БД selectedна кожну відповідь, а не завантажує їх вперед.
Застряг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.