Весьма распространено извлекать корневую сущность вместе с дочерними ассоциациями на нескольких уровнях.
В нашем примере нам нужно загрузить лес с его деревьями, ветвями и листьями, и мы попытаемся увидеть, как Hibernate ведет себя для трех типов коллекций: наборы, индексированные списки и пакеты.
Вот как выглядит наша иерархия классов:
Использование наборов и индексированных списков просто, поскольку мы можем загрузить все объекты, выполнив следующий запрос JPA-QL:
|
1
2
3
4
5
6
7
|
Forest f = entityManager.createQuery("select f " +"from Forest f " +"join fetch f.trees t " +"join fetch t.branches b " +"join fetch b.leaves l ", Forest.class).getSingleResult(); |
и выполненный запрос SQL:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
SELECT forest0_.id AS id1_7_0_, trees1_.id AS id1_18_1_, branches2_.id AS id1_4_2_, leaves3_.id AS id1_10_3_, trees1_.forest_fk AS forest_f3_18_1_, trees1_.index AS index2_18_1_, trees1_.forest_fk AS forest_f3_7_0__, trees1_.id AS id1_18_0__, trees1_.index AS index2_0__, branches2_.index AS index2_4_2_, branches2_.tree_fk AS tree_fk3_4_2_, branches2_.tree_fk AS tree_fk3_18_1__, branches2_.id AS id1_4_1__, branches2_.index AS index2_1__, leaves3_.branch_fk AS branch_f3_10_3_, leaves3_.index AS index2_10_3_, leaves3_.branch_fk AS branch_f3_4_2__, leaves3_.id AS id1_10_2__, leaves3_.index AS index2_2__FROM forest forest0_INNER JOIN tree trees1_ ON forest0_.id = trees1_.forest_fkINNER JOIN branch branches2_ ON trees1_.id = branches2_.tree_fkINNER JOIN leaf leaves3_ ON branches2_.id = leaves3_.branch_fk |
Но когда наши дочерние ассоциации отображаются как Bags, тот же запрос JPS-QL выдает «org.hibernate.loader.MultipleBagFetchException».
Если вы не можете изменить свои сопоставления (заменив пакеты на наборы или индексированные списки), у вас может возникнуть желание попробовать что-то вроде:
|
1
2
3
4
5
6
|
BagForest forest = entityManager.find(BagForest.class, forestId);for (BagTree tree : forest.getTrees()) { for (BagBranch branch : tree.getBranches()) { branch.getLeaves().size(); }} |
Но это неэффективно, генерируя множество SQL-запросов:
|
1
2
3
4
5
6
7
|
select trees0_.forest_id as forest_i3_1_1_, trees0_.id as id1_3_1_, trees0_.id as id1_3_0_, trees0_.forest_id as forest_i3_3_0_, trees0_.index as index2_3_0_ from BagTree trees0_ where trees0_.forest_id=? select branches0_.tree_id as tree_id3_3_1_, branches0_.id as id1_0_1_, branches0_.id as id1_0_0_, branches0_.index as index2_0_0_, branches0_.tree_id as tree_id3_0_0_ from BagBranch branches0_ where branches0_.tree_id=?select leaves0_.branch_id as branch_i3_0_1_, leaves0_.id as id1_2_1_, leaves0_.id as id1_2_0_, leaves0_.branch_id as branch_i3_2_0_, leaves0_.index as index2_2_0_ from BagLeaf leaves0_ where leaves0_.branch_id=? select leaves0_.branch_id as branch_i3_0_1_, leaves0_.id as id1_2_1_, leaves0_.id as id1_2_0_, leaves0_.branch_id as branch_i3_2_0_, leaves0_.index as index2_2_0_ from BagLeaf leaves0_ where leaves0_.branch_id=? select branches0_.tree_id as tree_id3_3_1_, branches0_.id as id1_0_1_, branches0_.id as id1_0_0_, branches0_.index as index2_0_0_, branches0_.tree_id as tree_id3_0_0_ from BagBranch branches0_ where branches0_.tree_id=?select leaves0_.branch_id as branch_i3_0_1_, leaves0_.id as id1_2_1_, leaves0_.id as id1_2_0_, leaves0_.branch_id as branch_i3_2_0_, leaves0_.index as index2_2_0_ from BagLeaf leaves0_ where leaves0_.branch_id=? select leaves0_.branch_id as branch_i3_0_1_, leaves0_.id as id1_2_1_, leaves0_.id as id1_2_0_, leaves0_.branch_id as branch_i3_2_0_, leaves0_.index as index2_2_0_ from BagLeaf leaves0_ where leaves0_.branch_id=? |
Итак, мое решение состоит в том, чтобы просто получить дочерние элементы самого низкого уровня и извлекать все необходимые ассоциации по всей иерархии сущностей.
Запуск этого кода:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
List<BagLeaf> leaves = transactionTemplate.execute(new TransactionCallback<List<BagLeaf>>() { @Override public List<BagLeaf> doInTransaction(TransactionStatus transactionStatus) { List<BagLeaf> leaves = entityManager.createQuery( "select l " + "from BagLeaf l " + "inner join fetch l.branch b " + "inner join fetch b.tree t " + "inner join fetch t.forest f " + "where f.id = :forestId", BagLeaf.class) .setParameter("forestId", forestId) .getResultList(); return leaves; }}); |
генерирует только один SQL-запрос:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
SELECT bagleaf0_.id AS id1_2_0_, bagbranch1_.id AS id1_0_1_, bagtree2_.id AS id1_3_2_, bagforest3_.id AS id1_1_3_, bagleaf0_.branch_id AS branch_i3_2_0_, bagleaf0_.index AS index2_2_0_, bagbranch1_.index AS index2_0_1_, bagbranch1_.tree_id AS tree_id3_0_1_, bagtree2_.forest_id AS forest_i3_3_2_, bagtree2_.index AS index2_3_2_FROM bagleaf bagleaf0_ INNER JOIN bagbranch bagbranch1_ ON bagleaf0_.branch_id = bagbranch1_.id INNER JOIN bagtree bagtree2_ ON bagbranch1_.tree_id = bagtree2_.id INNER JOIN bagforest bagforest3_ ON bagtree2_.forest_id = bagforest3_.idWHERE bagforest3_.id = ? |
Мы получаем список объектов Листа, но каждый Лист извлекает также Ветвь, которая выбирает Дерево, а затем и Лес. К сожалению, Hibernate не может волшебным образом создать иерархию вверх-вниз из результата запроса, подобного этому.
Попытка получить доступ к сумкам с:
|
1
|
leaves.get(0).getBranch().getTree().getForest().getTrees(); |
просто создает исключение LazyInitializationException, поскольку мы пытаемся получить доступ к неинициализированному списку отложенных прокси вне открытого контекста персистентности.
Итак, нам просто нужно заново воссоздать иерархию Forest из объектов List of Leaf.
И вот как я это сделал:
|
1
2
3
4
5
|
EntityGraphBuilder entityGraphBuilder = new EntityGraphBuilder(new EntityVisitor[] { BagLeaf.ENTITY_VISITOR, BagBranch.ENTITY_VISITOR, BagTree.ENTITY_VISITOR, BagForest.ENTITY_VISITOR}).build(leaves);ClassId<BagForest> forestClassId = new ClassId<BagForest>(BagForest.class, forestId);BagForest forest = entityGraphBuilder.getEntityContext().getObject(forestClassId); |
EntityGraphBuilder — это одна из написанных мной утилит, которая принимает массив объектов EntityVisitor и применяет их к посещаемым объектам. Это рекурсивно подходит к объекту Forest, и мы заменяем коллекции Hibernate новыми, добавляя каждого дочернего элемента в родительскую коллекцию дочерних элементов.
Поскольку дочерние коллекции были заменены, безопаснее не присоединять / объединять этот объект в новом контексте постоянства, так как все пакеты будут помечены как грязные.
Вот как организация использует своих посетителей:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
private <T extends Identifiable, P extends Identifiable> void visit(T object) { Class<T> clazz = (Class<T>) object.getClass(); EntityVisitor<T, P> entityVisitor = visitorsMap.get(clazz); if (entityVisitor == null) { throw new IllegalArgumentException("Class " + clazz + " has no entityVisitor!"); } entityVisitor.visit(object, entityContext); P parent = entityVisitor.getParent(object); if (parent != null) { visit(parent); }} |
И база EntityVisitor выглядит так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public void visit(T object, EntityContext entityContext) { Class<T> clazz = (Class<T>) object.getClass(); ClassId<T> objectClassId = new ClassId<T>(clazz, object.getId()); boolean objectVisited = entityContext.isVisited(objectClassId); if (!objectVisited) { entityContext.visit(objectClassId, object); } P parent = getParent(object); if (parent != null) { Class<P> parentClass = (Class<P>) parent.getClass(); ClassId<P> parentClassId = new ClassId<P>(parentClass, parent.getId()); if (!entityContext.isVisited(parentClassId)) { setChildren(parent); } List<T> children = getChildren(parent); if (!objectVisited) { children.add(object); } }} |
Этот код упакован как утилита, и настройка происходит через расширение EntityVisitors следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static EntityVisitor<BagForest, Identifiable> ENTITY_VISITOR = new EntityVisitor<BagForest, Identifiable>(BagForest.class) {};public static EntityVisitor<BagTree, BagForest> ENTITY_VISITOR = new EntityVisitor<BagTree, BagForest>(BagTree.class) { public BagForest getParent(BagTree visitingObject) { return visitingObject.getForest(); } public List<BagTree> getChildren(BagForest parent) { return parent.getTrees(); } public void setChildren(BagForest parent) { parent.setTrees(new ArrayList<BagTree>()); }};public static EntityVisitor<BagBranch, BagTree> ENTITY_VISITOR = new EntityVisitor<BagBranch, BagTree>(BagBranch.class) { public BagTree getParent(BagBranch visitingObject) { return visitingObject.getTree(); } public List<BagBranch> getChildren(BagTree parent) { return parent.getBranches(); } public void setChildren(BagTree parent) { parent.setBranches(new ArrayList<BagBranch>()); }};public static EntityVisitor<BagLeaf, BagBranch> ENTITY_VISITOR = new EntityVisitor<BagLeaf, BagBranch>(BagLeaf.class) { public BagBranch getParent(BagLeaf visitingObject) { return visitingObject.getBranch(); } public List<BagLeaf> getChildren(BagBranch parent) { return parent.getLeaves(); } public void setChildren(BagBranch parent) { parent.setLeaves(new ArrayList<BagLeaf>()); }}; |
Это не шаблон посетителя как таковой, но он имеет небольшое сходство с ним. Хотя всегда лучше просто использовать индексированные списки или наборы, вы все равно можете получить свой график связей, используя также один запрос для Bags.
- Код доступен на GitHub .
