Статьи

Факты гибернации: многоуровневая загрузка

Весьма распространено извлекать корневую сущность вместе с дочерними ассоциациями на нескольких уровнях.

В нашем примере нам нужно загрузить лес с его деревьями, ветвями и листьями, и мы попытаемся увидеть, как 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 .