Загрузить рекурсивный граф объектов без N + 1 декартова продукта с JPA и Hibernate

При преобразовании проекта из Ibatis в JPA 2.1 я столкнулся с проблемой, когда мне приходится загружать полный графический объект для набора объектов, не ударяя N + 1, выбирая или используя декартовы продукты по соображениям производительности.

Пользовательский запрос даст List <Task> , и мне нужно убедиться, что когда я возвращаю задачи, у них есть все свойства, включая parent, children, зависимости и свойства. Сначала позвольте мне объяснить два объекта объекта.

Задача является частью иерархии. Он может иметь родительскую задачу, и у нее также могут быть дети. Задача может зависеть от других задач, выражаемых свойством "зависимости". Задача может иметь много свойств, выражаемых свойством properties.

Примеры объектов были максимально упрощены, и код шаблона удален.

@Entity
public class Task {
    @Id
    private Long id;

    @ManyToOne(fetch = LAZY)
    private Task parent;

    @ManyToOne(fetch = LAZY)
    private Task root;

    @OneToMany(mappedBy = "task")
    private List<TaskProperty> properties;

    @ManyToMany
    @JoinTable(name = "task_dependency", inverseJoinColumns = { @JoinColumn(name = "depends_on")})
    private List<Task> dependencies;

    @OneToMany(mappedBy = "parent")
    private List<Task> children;
}

@Entity
public class TaskPropertyValue {
    @Id
    private Long id;

    @ManyToOne(fetch = LAZY)
    private Task task;

    private String name;
    private String value;
}

Иерархия задач для заданной задачи может быть бесконечно глубокой, поэтому для упрощения получения всего графика задача будет иметь указатель на ее корневую задачу с помощью свойства "root".

В Ibatis я просто взял все Задачи для отдельного списка корневых идентификаторов, а затем сделал ad-hoc-запросы для всех свойств и зависимостей с запросом "task_id IN()". Когда у меня были такие, я использовал Java-код для добавления свойств, дочерних элементов и зависимостей ко всем объектам модели, чтобы график был завершен. Для любого списка задач по размеру я бы тогда сделал только 3 SQL-запроса, и я пытаюсь сделать то же самое с JPA. Поскольку свойство "parent" указывает, где добавить дочерние элементы, мне даже не пришлось запрашивать их.

Я пробовал разные подходы, в том числе:

Пусть ленивая загрузка сделает это

  • Эксплуатационное самоубийство, не нужно уточнять:)

ПРИСОЕДИНЯЙТЕСЬ ПОЛЬЗОВАТЕЛЕЙ, ПРИСОЕДИНЯЙТЕСЬ НА FETCH, НАПРАВЛЯЙТЕ НАСТРОЙКИ FETCH

  • Это проблематично, потому что полученные декартовы продукты огромны, и моя реализация JPA (Hibernate) не поддерживает List, только Set при наборе нескольких пакетов. Задача может иметь огромное количество свойств, что делает декартовы продукты неэффективными.

Запросы ad-hoc так же, как и в ibatis

  • Я не могу добавить дочерние элементы, зависимости и свойства к Lazy инициализированным коллекциям объектов Task, потому что Hibernate затем попытается добавить их в качестве новых объектов.

Одним из возможных решений может быть создание новых объектов Task, которые не управляются JPA, и сшить мою иерархию вместе с ними, и я думаю, что я могу жить с этим, но это не очень "JPA", а затем я не может использовать JPA для того, что хорошо подходит для отслеживания и сохранения изменений в моих объектах автоматически.

Любые подсказки будут очень признательны. Я открыт для использования специальных расширений поставщика, если это необходимо. Я работаю в Wildfly 8.1.0.Final(полный профиль Java EE7) с Hibernate 4.3.5.Final.

Ответы

Ответ 1

Доступные Варианты

Есть несколько стратегий для достижения ваших целей:

  • При выборке из-под-выбора все ленивые объекты будут загружены дополнительным суб-выбором, в самый первый раз, когда вам понадобится ленивая ассоциация данного типа. Этот звук на первый взгляд привлекателен, но он делает ваше приложение хрупким по отношению к количеству дополнительных суб-выбранных объектов для выборки и может распространяться на другие методы обслуживания.

  • Пакетную выборку легче контролировать, так как вы можете принудительно установить количество объектов, загружаемых в один пакет, и это может не повлиять на другие варианты использования.

  • использование рекурсивного общего табличного выражения, если ваш db поддерживает его.

Планируйте заранее

В конце концов, все зависит от того, что вы планируете делать с выбранными строками. Если просто отобразить их в виде, более чем достаточно встроенного запроса.

Если вам нужно сохранить сущности по нескольким запросам (первая часть просмотра, вторая для части обновления), тогда лучше использовать сущности.

Из вашего ответа я вижу, что вам необходимо выполнить EntityManager.merge() и, вероятно, полагаться на каскад, чтобы распространять дочерние переходы состояний (добавлять/удалять).

Поскольку мы говорим о 3 запросах JPA и до тех пор, пока вы не получите декартово произведение, у вас все будет в порядке с JPA.

Заключение

Вы должны стремиться к минимальному количеству запросов, но это не значит, что у вас всегда будет один и только один запрос. Два или три запроса не проблема вообще.

Пока вы контролируете количество запросов и не сталкиваетесь с проблемой N + 1, у вас все в порядке с более чем одним запросом. Торговля Декартовым продуктом (2 выборки "один ко многим") за одно объединение и один дополнительный выбор - хорошая сделка.

В конце вы всегда должны проверять план запроса EXPLAIN ANALYZE и подкреплять/переосмысливать свою стратегию.