Hibernate是一个很棒的ORM工具,它极大地简化了开发,但是如果您想正确地使用它,则有很多陷阱。
在大中型项目中,双向父子关联非常常见,这使我们能够浏览给定关系的两端。
在控制关联的持久/合并部分时,有两个可用选项。 其中将有负责同步收集变化的@OneToMany结束,但是这是一个低效率的做法,这是很好的描述在这里 。
最常见的方法是@ManyToOne端控制关联并且@OneToMany端使用“ mappedBy”选项时。
我将讨论后一种方法,因为就执行的查询数量而言,这是最常见,最有效的方法。
因此,对于双向集合,我们可以使用java.util.List或java.util.Set。
根据Hibernate docs的说法,列表和文件包比集合更有效。
但是当我看到以下代码时,我仍然感到焦虑:
@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年在许多项目上看到这一点后,您了解了为什么我对使用列表与集合持怀疑态度。
这是在运行复制此问题的测试用例时得到的结果,因此添加两个子级,我们得到:
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,因为它只影响合并操作,而不影响持久化操作。
但是所有这些都是黑客,在大型项目中很难遵循,因为许多开发人员都在相同的代码库上工作。
因此,我的首选方式是使用Set,即使有时它们的效率不如Lists更好,但是由于我一直偏爱正确性与性能优化,因此最好使用Set。
当涉及到这类问题时,最好具有代码约定,因为它们易于添加到项目开发指南中,并且易于记忆和采用。
使用集合的一个优点是,它迫使您定义适当的equals / hashCode策略(该策略应始终包括实体的业务密钥。业务密钥是一种字段组合,该字段组合在父级的子级中是唯一的或唯一的,并且甚至在之前也是一致的)以及将实体持久保存到数据库中之后)。
如果您担心会失去以添加孩子的相同顺序保存孩子的“列表”功能,那么您仍然可以为Sets模仿。
默认情况下,集合是无序的和未排序的,但是即使您不能对它们进行排序,也可以通过使用@OrderBy JPA注释按给定的列对它们进行排序,如下所示:
@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类似于:
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
结论:
如果您的领域模型要求使用列表而不是集合,则将打破您的约束,不允许重复。 但是,如果您需要重复项,则仍然可以使用索引列表。 据说Bag是未排序且“无序的”(即使它按照在数据库表中添加子的顺序来检索子)。 因此,索引列表也将是一个不错的选择,对吗?
我还想提请注意一个5年的bug,它影响了多个Hibernate版本,并且是我在多个项目中复制的一个版本。 当然,有一些解决方法,例如删除Cascade.Merge或合并Children vs the Parent,但是有许多开发人员不知道此问题及其解决方法。
根据Hibernate docs:集是“ 表示多值关联的推荐方法 ”,而且我已经看到很多情况下使用Bags作为默认双向集合,即使无论如何集都是更好的选择。
因此,我仍然对Bags保持谨慎,如果我的领域模型强加使用List,我总是会选择索引的。
- 代码可在GitHub上获得 。
翻译自: https://www.javacodegeeks.com/2013/10/hibernate-facts-favoring-bidirectional-sets-vs-lists.html