根据其javadoc,java.util.List应该代表一个有序的值集合。但如果你把它作为to-many关联或ElementCollection的类型,就不一定是这样了。正如我之前解释的,Hibernate可以将java.util.List作为一个Bag或List来处理。只有List映射会持久化其元素的顺序。但是默认情况下,Hibernate将java.util.List作为一个Bag来处理。当你从数据库中获取它时,它包含的元素的顺序是未定义的。
在Hibernate 6.0.0之前,你必须给你的*ElementCollection、一对多关联或多对多关联的拥有方添加一个@OrderColumn注解,以持久化java.util.List*的顺序。然后,Hibernate将每个元素的位置持久化在一个单独的列中,并在所有插入、更新和删除操作中管理所有元素的索引。
请注意,@OrderColumn注解不支持 多对多关联的引用方*(@ManyToMany(mappedBy = "...")*)。
从Hibernate 6开始,你可以通过在persistence.xml中设置配置属性hibernate.mapping.default_list_semantics为LIST,为多对多关联和ElementCollections的拥有方全面定义List语义。如果你想对你的一对多关联应用同样的处理方法,你仍然需要添加一个*@OrderColumn*注解。
让我们仔细看看这个新的配置参数以及管理和持久化List中每个元素的索引的意义。
内容
默认情况下,Hibernate不会持久化任何ElementCollection 或to-many关联中元素的顺序。你可以通过两种方式来改变它。你可以全局地配置它,或者调整对特定属性的处理。当这样做的时候,请记住全局设置并不影响一对多或多对多关联的引用方。
改变全局List的语义
从Hibernate 6.0.0开始,你可以在你的persistence.xml配置中把属性hibernate.mapping.default_list_semantics 设置为LIST:
<persistence>
<persistence-unit name="my-persistence-unit">
<description>Hibernate example configuration - thorben-janssen.com</description>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.mapping.default_list_semantics" value="LIST" />
...
</properties>
</persistence-unit>
</persistence>
然后,Hibernate将你的ElementCollection 和你的多对多关联的拥有方以相同的方式处理,就像你用*@OrderColumn注释它们一样。它将每个元素的位置持久化在一个单独的数据库列中。为了获得该列的名称,Hibernate在映射关联的列的名称中加入后缀_ORDER*。
让我们在基于以下ChessPlayer 和ChessTournament 实体的一个简单测试场景中使用这个方法:
@Entity
public class ChessPlayer {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "player_seq")
private Long id;
private String firstName;
private String lastName;
private LocalDate birthDate;
@OneToMany(mappedBy = "playerWhite")
private Set<ChessGame> gamesWhite;
@OneToMany(mappedBy = "playerBlack")
private Set<ChessGame> gamesBlack;
@Version
private int version;
...
}
@Entity
public class ChessTournament {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tournament_seq")
private Long id;
private String name;
private LocalDate startDate;
private LocalDate endDate;
@Version
private int version;
@ManyToMany
private List<ChessPlayer> players = new ArrayList<ChessPlayer>();
@OneToMany
private Set<ChessGame> games = new HashSet<>();
...
}
正如你所看到的,这两个实体类没有什么特别之处。两者都使用数据库序列来生成它们的主键值,以及用于乐观锁定的版本属性。配置的列表语义将影响ChessTournament 实体的player 属性所模拟的多对多的关联。
当你执行下面的测试案例时,你可以看到Hibernate将每个ChessPlayer的位置存储在List players的player_ORDER 列中。
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ChessTournament tournament = em.find(ChessTournament.class, 1L);
ChessPlayer player1 = em.find(ChessPlayer.class, 1L);
ChessPlayer player2 = em.find(ChessPlayer.class, 2L);
ChessPlayer player3 = em.find(ChessPlayer.class, 3L);
tournament.getPlayers().add(player1);
tournament.getPlayers().add(player2);
tournament.getPlayers().add(player3);
em.getTransaction().commit();
em.close();
18:13:54,250 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.endDate,c1_0.name,c1_0.startDate,c1_0.version from ChessTournament c1_0 where c1_0.id=?
18:13:54,277 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
18:13:54,281 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
18:13:54,284 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
18:13:54,295 DEBUG [org.hibernate.SQL] - select p1_0.ChessTournament_id,p1_0.players_ORDER,p1_1.id,p1_1.birthDate,p1_1.firstName,p1_1.lastName,p1_1.version from ChessTournament_ChessPlayer p1_0 join ChessPlayer p1_1 on p1_1.id=p1_0.players_id where p1_0.ChessTournament_id=?
18:13:54,326 DEBUG [org.hibernate.SQL] - update ChessTournament set endDate=?, name=?, startDate=?, version=? where id=? and version=?
18:13:54,334 DEBUG [org.hibernate.SQL] - insert into ChessTournament_ChessPlayer (ChessTournament_id, players_ORDER, players_id) values (?, ?, ?)
18:13:54,340 DEBUG [org.hibernate.SQL] - insert into ChessTournament_ChessPlayer (ChessTournament_id, players_ORDER, players_id) values (?, ?, ?)
18:13:54,343 DEBUG [org.hibernate.SQL] - insert into ChessTournament_ChessPlayer (ChessTournament_id, players_ORDER, players_id) values (?, ?, ?)
当Hibernate获取玩家列表时,它没有使用ORDER BY 子句来获取玩家的正确顺序,而是在内存中排序。
18:20:49,230 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.endDate,c1_0.name,c1_0.startDate,c1_0.version from ChessTournament c1_0 where c1_0.id=?
18:20:49,234 DEBUG [org.hibernate.SQL] - select p1_0.ChessTournament_id,p1_0.players_ORDER,p1_1.id,p1_1.birthDate,p1_1.firstName,p1_1.lastName,p1_1.version from ChessTournament_ChessPlayer p1_0 join ChessPlayer p1_1 on p1_1.id=p1_0.players_id where p1_0.ChessTournament_id=?
调整特定的列表语义
如果你只想持久化特定的ElementCollection 或多对多关联的元素的顺序,或者你想持久化一对多关联的顺序,你需要用*@OrderColumn注解来注释该属性。在这样做的时候,你可以提供订单列的名称,或者使用默认的,即在映射关联的列的名称中添加后缀_ORDER*。
@Entity
public class ChessPlayer {
@OneToMany(mappedBy = "playerWhite")
@OrderColumn
private Set<ChessGame> gamesWhite;
@OneToMany(mappedBy = "playerBlack")
@OrderColumn(name="myOrderColumn")
private Set<ChessGame> gamesBlack;
...
}
然后,Hibernate以与前面例子相同的方式处理关联。它将每个元素的索引持久化在一个单独的列中。在这个例子中,Hibernate为gamesWhite属性使用默认名称gamesWhite_ORDER ,为gamesBlack属性使用由*@OrderColumn注解指定的myOrderColumn*。
持久保存元素的顺序的影响
持久保存关联中元素的顺序可能听起来是个好主意。但是它需要一个管理的工作,会减慢你的写操作。因为Hibernate持久化了每个元素的位置,如果你要添加或删除任何不是List中最后一个的元素,它必须更新多个记录。
在前面的例子中,我把一个元素加到了List的最后。这并不要求对其他元素的索引做任何改变。但是当你在中间的某个地方添加一个元素时,情况就会改变。
tournament = em.find(ChessTournament.class, 1L);
ChessPlayer player4 = em.find(ChessPlayer.class, 4L);
tournament.getPlayers().add(1, player4);
通过在位置1上添加ChessPlayer ,所有在位置1或更高的元素的索引都被增加了一个。然后Hibernate需要在数据库中进行更新。
18:32:07,152 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.endDate,c1_0.name,c1_0.startDate,c1_0.version from ChessTournament c1_0 where c1_0.id=?
18:32:07,159 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
18:32:07,164 DEBUG [org.hibernate.SQL] - select p1_0.ChessTournament_id,p1_0.players_ORDER,p1_1.id,p1_1.birthDate,p1_1.firstName,p1_1.lastName,p1_1.version from ChessTournament_ChessPlayer p1_0 join ChessPlayer p1_1 on p1_1.id=p1_0.players_id where p1_0.ChessTournament_id=?
18:32:07,177 DEBUG [org.hibernate.SQL] - update ChessTournament set endDate=?, name=?, startDate=?, version=? where id=? and version=?
18:32:07,183 DEBUG [org.hibernate.SQL] - update ChessTournament_ChessPlayer set players_id=? where ChessTournament_id=? and players_ORDER=?
18:32:07,187 DEBUG [org.hibernate.SQL] - update ChessTournament_ChessPlayer set players_id=? where ChessTournament_id=? and players_ORDER=?
18:32:07,191 DEBUG [org.hibernate.SQL] - insert into ChessTournament_ChessPlayer (ChessTournament_id, players_ORDER, players_id) values (?, ?, ?)
当然,如果你删除任何元素,除了最后一个,也会发生同样的情况。
正如你在这个例子中所看到的,持久化List 的顺序会产生一个开销。根据List的大小和你执行的操作种类,这可能会导致严重的性能问题。因此,我建议坚持使用Hibernate的旧的List语义,并非常谨慎地使用新的处理方法。
结论
List应该是一个元素的有序集合。但在默认情况下,Hibernate并没有坚持这个顺序,数据库会以随机的顺序返回元素。因此,在Hibernate从数据库中获取List 后,元素的初始顺序会丢失。
在Hibernate 6之前,你可以通过在ElementCollection、一对多关联或多对多关联的拥有方上注解*@OrderColumn*来避免这种情况。然后,Hibernate将每个元素的位置存储在一个单独的数据库列中。
从Hibernate 6开始,你可以使用一个新的配置参数来改变对所有ElementCollections和多对多关联的拥有方的Lists的全局处理。要做到这一点,你只需要将hibernate.mapping.default_list_semantics配置属性设置为LIST。
当你决定持久化你的Lists的顺序时,请记住,这可能会减慢你的写操作。Hibernate需要管理所有List元素的索引,如果你插入或删除一个不是最后一个的元素,就需要进行多次更新操作。