Загрузить рекурсивный граф объектов без 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 и подкреплять/переосмысливать свою стратегию.