Весьма распространено извлекать корневую сущность вместе с дочерними ассоциациями на нескольких уровнях.
В нашем примере нам нужно загрузить лес с его деревьями, ветвями и листьями, и мы попытаемся увидеть, как 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 | SELECTforest0_.id        ASid1_7_0_,       trees1_.id         ASid1_18_1_,       branches2_.id      ASid1_4_2_,       leaves3_.id        ASid1_10_3_,       trees1_.forest_fk  ASforest_f3_18_1_,       trees1_.indexASindex2_18_1_,       trees1_.forest_fk  ASforest_f3_7_0__,       trees1_.id         ASid1_18_0__,       trees1_.indexASindex2_0__,       branches2_.indexASindex2_4_2_,       branches2_.tree_fk AStree_fk3_4_2_,       branches2_.tree_fk AStree_fk3_18_1__,       branches2_.id      ASid1_4_1__,       branches2_.indexASindex2_1__,       leaves3_.branch_fk ASbranch_f3_10_3_,       leaves3_.indexASindex2_10_3_,       leaves3_.branch_fk ASbranch_f3_4_2__,       leaves3_.id        ASid1_10_2__,       leaves3_.indexASindex2_2__FROMforest forest0_INNERJOINtree trees1_ ONforest0_.id = trees1_.forest_fkINNERJOINbranch branches2_ ONtrees1_.id = branches2_.tree_fkINNERJOINleaf leaves3_ ONbranches2_.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 | selecttrees0_.forest_id asforest_i3_1_1_, trees0_.id asid1_3_1_, trees0_.id asid1_3_0_, trees0_.forest_id asforest_i3_3_0_, trees0_.indexasindex2_3_0_ fromBagTree trees0_ wheretrees0_.forest_id=?               selectbranches0_.tree_id astree_id3_3_1_, branches0_.id asid1_0_1_, branches0_.id asid1_0_0_, branches0_.indexasindex2_0_0_, branches0_.tree_id astree_id3_0_0_ fromBagBranch branches0_ wherebranches0_.tree_id=?selectleaves0_.branch_id asbranch_i3_0_1_, leaves0_.id asid1_2_1_, leaves0_.id asid1_2_0_, leaves0_.branch_id asbranch_i3_2_0_, leaves0_.indexasindex2_2_0_ fromBagLeaf leaves0_ whereleaves0_.branch_id=?        selectleaves0_.branch_id asbranch_i3_0_1_, leaves0_.id asid1_2_1_, leaves0_.id asid1_2_0_, leaves0_.branch_id asbranch_i3_2_0_, leaves0_.indexasindex2_2_0_ fromBagLeaf leaves0_ whereleaves0_.branch_id=?        selectbranches0_.tree_id astree_id3_3_1_, branches0_.id asid1_0_1_, branches0_.id asid1_0_0_, branches0_.indexasindex2_0_0_, branches0_.tree_id astree_id3_0_0_ fromBagBranch branches0_ wherebranches0_.tree_id=?selectleaves0_.branch_id asbranch_i3_0_1_, leaves0_.id asid1_2_1_, leaves0_.id asid1_2_0_, leaves0_.branch_id asbranch_i3_2_0_, leaves0_.indexasindex2_2_0_ fromBagLeaf leaves0_ whereleaves0_.branch_id=?        selectleaves0_.branch_id asbranch_i3_0_1_, leaves0_.id asid1_2_1_, leaves0_.id asid1_2_0_, leaves0_.branch_id asbranch_i3_2_0_, leaves0_.indexasindex2_2_0_ fromBagLeaf leaves0_ whereleaves0_.branch_id=? | 
Итак, мое решение состоит в том, чтобы просто получить дочерние элементы самого низкого уровня и извлекать все необходимые ассоциации по всей иерархии сущностей.
Запуск этого кода:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | List<BagLeaf> leaves = transactionTemplate.execute(newTransactionCallback<List<BagLeaf>>() {    @Override    publicList<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();        returnleaves;    }}); | 
генерирует только один SQL-запрос:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | SELECTbagleaf0_.id        ASid1_2_0_,       bagbranch1_.id      ASid1_0_1_,       bagtree2_.id        ASid1_3_2_,       bagforest3_.id      ASid1_1_3_,       bagleaf0_.branch_id ASbranch_i3_2_0_,       bagleaf0_.indexASindex2_2_0_,       bagbranch1_.indexASindex2_0_1_,       bagbranch1_.tree_id AStree_id3_0_1_,       bagtree2_.forest_id ASforest_i3_3_2_,       bagtree2_.indexASindex2_3_2_FROMbagleaf bagleaf0_       INNERJOINbagbranch bagbranch1_               ONbagleaf0_.branch_id = bagbranch1_.id       INNERJOINbagtree bagtree2_               ONbagbranch1_.tree_id = bagtree2_.id       INNERJOINbagforest bagforest3_               ONbagtree2_.forest_id = bagforest3_.idWHEREbagforest3_.id = ? | 
Мы получаем список объектов Листа, но каждый Лист извлекает также Ветвь, которая выбирает Дерево, а затем и Лес. К сожалению, Hibernate не может волшебным образом создать иерархию вверх-вниз из результата запроса, подобного этому.
Попытка получить доступ к сумкам с:
| 1 | leaves.get(0).getBranch().getTree().getForest().getTrees(); | 
просто создает исключение LazyInitializationException, поскольку мы пытаемся получить доступ к неинициализированному списку отложенных прокси вне открытого контекста персистентности.
Итак, нам просто нужно заново воссоздать иерархию Forest из объектов List of Leaf.
И вот как я это сделал:
| 1 2 3 4 5 | EntityGraphBuilder entityGraphBuilder = newEntityGraphBuilder(newEntityVisitor[] {        BagLeaf.ENTITY_VISITOR, BagBranch.ENTITY_VISITOR, BagTree.ENTITY_VISITOR, BagForest.ENTITY_VISITOR}).build(leaves);ClassId<BagForest> forestClassId = newClassId<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 extendsIdentifiable, P extendsIdentifiable> voidvisit(T object) {    Class<T> clazz = (Class<T>) object.getClass();    EntityVisitor<T, P> entityVisitor = visitorsMap.get(clazz);    if(entityVisitor == null) {        thrownewIllegalArgumentException("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 | publicvoidvisit(T object, EntityContext entityContext) {    Class<T> clazz = (Class<T>) object.getClass();    ClassId<T> objectClassId = newClassId<T>(clazz, object.getId());    booleanobjectVisited = 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 = newClassId<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 | publicstaticEntityVisitor<BagForest, Identifiable> ENTITY_VISITOR = newEntityVisitor<BagForest, Identifiable>(BagForest.class) {};publicstaticEntityVisitor<BagTree, BagForest> ENTITY_VISITOR = newEntityVisitor<BagTree, BagForest>(BagTree.class) {    publicBagForest getParent(BagTree visitingObject) {        returnvisitingObject.getForest();    }    publicList<BagTree> getChildren(BagForest parent) {        returnparent.getTrees();    }    publicvoidsetChildren(BagForest parent) {        parent.setTrees(newArrayList<BagTree>());    }};publicstaticEntityVisitor<BagBranch, BagTree> ENTITY_VISITOR = newEntityVisitor<BagBranch, BagTree>(BagBranch.class) {    publicBagTree getParent(BagBranch visitingObject) {        returnvisitingObject.getTree();    }    publicList<BagBranch> getChildren(BagTree parent) {        returnparent.getBranches();    }    publicvoidsetChildren(BagTree parent) {        parent.setBranches(newArrayList<BagBranch>());    }};publicstaticEntityVisitor<BagLeaf, BagBranch> ENTITY_VISITOR = newEntityVisitor<BagLeaf, BagBranch>(BagLeaf.class) {    publicBagBranch getParent(BagLeaf visitingObject) {        returnvisitingObject.getBranch();    }    publicList<BagLeaf> getChildren(BagBranch parent) {        returnparent.getLeaves();    }    publicvoidsetChildren(BagBranch parent) {        parent.setLeaves(newArrayList<BagLeaf>());    }}; | 
Это не шаблон посетителя как таковой, но он имеет небольшое сходство с ним. Хотя всегда лучше просто использовать индексированные списки или наборы, вы все равно можете получить свой график связей, используя также один запрос для Bags.
- Код доступен на GitHub .
