Весьма распространено извлекать корневую сущность вместе с дочерними ассоциациями на нескольких уровнях.
В нашем примере нам нужно загрузить лес с его деревьями, ветвями и листьями, и мы попытаемся увидеть, как 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_fk INNER JOIN branch branches2_ ON trees1_.id = branches2_.tree_fk INNER 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_.id WHERE 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 .