Статьи

Факты гибернации: предпочтительные двунаправленные наборы против списков

Hibernate — отличный инструмент ORM, и он значительно облегчает разработку, но в нем есть много ошибок, о которых вы должны знать, если хотите правильно его использовать.

В средних и крупных проектах очень часто встречаются двунаправленные ассоциации родитель-потомок, которые позволяют нам ориентироваться на обоих концах данного отношения.

Когда дело доходит до управления частью ассоциации с постоянным / слиянием, доступны две опции. Можно было бы поручить конечной точке @OneToMany синхронизировать изменения коллекции, но это неэффективный подход, который очень хорошо описан здесь .

Наиболее распространенный подход — когда сторона @ManyToOne контролирует связь, а конец @OneToMany использует опцию «mappedBy».

Я буду обсуждать последний подход, поскольку он является наиболее распространенным и наиболее эффективным с точки зрения количества выполненных запросов.

Таким образом, для двунаправленных коллекций мы можем использовать java.util.List или java.util.Set.

Согласно документам Hibernate , списки и сумки более эффективны, чем наборы.

Но я все еще волнуюсь, когда вижу следующий код:

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
44
45
46
47
48
@Entity
public class Parent {
 
...
 
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent", orphanRemoval = true)
private List children = new ArrayList()
 
public List getChildren() {
return children;
}
 
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
 
public void removeChild(Child child) {
children.remove(child);
child.setParent(null);
}
}
 
@Entity
public class Child {
 
...
 
@ManyToOne
private Parent parent;
 
public Parent getParent() {
return parent;
}
 
public void setParent(Parent parent) {
this.parent = parent;
}
}
 
Parent parent = loadParent(parentId);
Child child1 = new Child();
child1.setName("child1");
Child child2 = new Child();
child2.setName("child2");
parent.addChild(child1);
parent.addChild(child2);
entityManager.merge(parent);

Это потому, что последние пять лет я вставляю дубликаты дочерних элементов, когда вызывается операция объединения родительской ассоциации. Это происходит из-за следующих проблем: HHH-3332 и HHH-5855 .

В последнее время я тестировал некоторые версии Hibernate, и они все еще копируются в версиях 3.5.6, 3.6.10 и 4.2.6. Итак, после 5 лет просмотра этого во многих проектах вы понимаете, почему я скептически отношусь к использованию списков и наборов.

Это то, что я получаю, когда запускаю тестовый пример, повторяющий эту проблему, поэтому для добавления двух дочерних элементов мы получаем:

1
2
3
4
5
select parent0_.id as id1_2_0_ from Parent parent0_ where parent0_.id=?
insert into Child (id, name, parent_id) values (default, ?, ?)
insert into Child (id, name, parent_id) values (default, ?, ?)
insert into Child (id, name, parent_id) values (default, ?, ?)
insert into Child (id, name, parent_id) values (default, ?, ?)

Эта проблема реплицируется только в том случае, если операции слияния каскадно переходят из родительских в дочерние и существуют обходные пути, например:

  • слияние ребенка вместо родителя
  • сохраняя детей до слияния родителей
  • удаление Cascade.ALL или Cascade.MERGE из родительского, так как это влияет только на операцию слияния, а не на постоянную.

Но все это взломы, и за ними очень трудно следовать в крупномасштабном проекте, когда многие разработчики работают над одной и той же кодовой базой.

Поэтому я предпочитаю использовать наборы, даже если иногда они будут менее эффективными, чем списки, но, поскольку я всегда предпочитаю корректность по сравнению с оптимизацией производительности, мне лучше использовать наборы.

Когда дело доходит до такого рода проблем, хорошо иметь условные обозначения кода, поскольку их легко добавить в руководство по разработке проекта, а также их легче запомнить и принять.

Одно из преимуществ использования Sets заключается в том, что оно заставляет вас определять правильную стратегию equals / hashCode (которая всегда должна включать в себя бизнес-ключ сущности. Бизнес-ключ — это комбинации полей, которые уникальны или уникальны среди дочерних элементов родителя, и которые согласованы даже раньше и после того, как сущность сохраняется в базе данных).

Если вы обеспокоены тем, что потеряете возможность сохранения списка в том же порядке, в котором вы их добавили, то вы все равно можете эмулировать это и для наборов.

По умолчанию наборы неупорядочены и не отсортированы, но даже если вы не можете упорядочить их, вы все равно можете отсортировать их по заданному столбцу, используя аннотацию @OrderBy JPA, например:

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
@Entity
public class LinkedParent {
 
...
 
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent", orphanRemoval = true)
@OrderBy("id")
private Set children = new LinkedHashSet();
 
...
 
public Set getChildren() {
return children;
}
 
public void addChild(LinkedChild child) {
children.add(child);
child.setParent(this);
}
 
public void removeChild(LinkedChild child) {
children.remove(child);
child.setParent(null);
}
}

Когда дочерние элементы родителя загружены, сгенерированный SQL выглядит так:

1
select children0_.parent_id as parent_i3_3_1_, children0_.id as id1_2_1_, children0_.id as id1_2_0_, children0_.name as name2_2_0_, children0_.parent_id as parent_i3_2_0_ from LinkedChild children0_ where children0_.parent_id=? order by children0_.id

Вывод:

Если ваша модель домена требует использования List, то Set нарушит ваши ограничения, запретив дубликаты. Но если вам нужны дубликаты, вы все равно можете использовать индексированный список. Говорят, что Bag является несортированным и «неупорядоченным» (даже если он извлекает дочерние элементы в порядке их добавления в таблицу базы данных). Таким образом, индексированный список также будет хорошим кандидатом, верно?

Я также хотел привлечь внимание к пятилетней ошибке, затрагивающей несколько версий Hibernate, и той, которую я повторил в нескольких проектах. Конечно, есть обходные пути, такие как удаление Cascade.Merge или слияние детей с родителем, но многие разработчики не знают об этой проблеме и ее обходных путях.

Согласно Hibernate docs: Наборы являются « рекомендуемым способом представления многозначных ассоциаций », и я видел много случаев, когда Bags использовались в качестве двунаправленной коллекции по умолчанию, даже если набор был бы лучшим выбором в любом случае.

Поэтому я все еще осторожен с Bags, и если моя доменная модель предполагает использование List, я всегда выбираю индексированный.

  • Код доступен на GitHub .