|

How to configure List semantics in Hibernate 6


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Based on its javadoc, a java.util.List is supposed to represent an ordered collection of values. But that’s not necessarily the case if you use it as the type of a to-many association or an ElementCollection. As I explained before, Hibernate can handle a java.util.List as a Bag or a List. Only the List mapping persists the order of its elements. But by default, Hibernate handles a java.util.List as a Bag. After you have fetched it from the database, it contains the elements in an undefined order.

Until Hibernate 6.0.0, you had to add an @OrderColumn annotation to your ElementCollection, one-to-many association, or the owning side of your many-to-many association to persist the order of your java.util.List. Hibernate then persists the position of each element in a separate column and manages the index of all elements during all insert, update, and delete operations.

Please note that the @OrderColumn annotation is not supported for the referencing side of many-to-many associations (@ManyToMany(mappedBy = “…”)).

Since Hibernate 6, you can define the List semantics globally for the owning side of your many-to-many associations and ElementCollections by setting the configuration property hibernate.mapping.default_list_semantics to LIST in your persistence.xml. If you want to apply the same handling to your one-to-many associations, you still have to add an @OrderColumn annotation.

Let’s take a closer look at this new configuration parameter and the implications of managing and persisting the index of each element in the List.

How to configure Hibernate’s List semantics

By default, Hibernate doesn’t persist the order of the elements of any ElementCollection or to-many association. You can change that in 2 ways. You can either configure it globally or adjust the handling of a specific attribute. When doing that, please keep in mind that the global setting doesn’t affect the referencing side of a one-to-many or many-to-many association.

Changing global List semantics

Since Hibernate 6.0.0, you can set the property hibernate.mapping.default_list_semantics in your persistence.xml configuration to 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 then treats your ElementCollection and the owning side of your many-to-many associations in the same ways as if you annotated them with @OrderColumn. It persists the position of each element in a separate database column. To get the name of that column, Hibernate adds the postfix _ORDER to the name of the column that maps the association.

Let’s use this in a simple test scenario based on the following ChessPlayer and ChessTournament entities.

@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<>();
	
	...
}

As you can see, there is nothing special about these 2 entity classes. Both use a database sequence to generate their primary key values and a version attribute for optimistic locking. The configured list semantics will affect the many-to-many association modeled by the players attribute of the ChessTournament entity.

When you execute the following test case, you can see that Hibernate stores each ChessPlayer‘s position in the List players in the players_ORDER column.

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 (?, ?, ?)

When Hibernate fetches the list of players, it doesn’t use an ORDER BY clause to get the players in the right order but orders them in memory instead.

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=?

Adjusting specific List semantics

If you only want to persist the order of the elements of a specific ElementCollection or many-to-many association, or if you want to persist the order of a one-to-many association, you need to annotate the attribute with an @OrderColumn annotation. When doing that, you can either provide the name of the order column or use the default, which adds the postfix _ORDER to the name of the column that maps the association.

@Entity
public class ChessPlayer {

    @OneToMany(mappedBy = "playerWhite")
    @OrderColumn
    private Set<ChessGame> gamesWhite;

    @OneToMany(mappedBy = "playerBlack")
    @OrderColumn(name="myOrderColumn")
    private Set<ChessGame> gamesBlack;
	
	...
}

Hibernate then handles the association in the same way as in the previous example. It persists the index of each element in a separate column. In this example, Hibernate uses the default name gamesWhite_ORDER for the gamesWhite attribute and the myOrderColumn specified by the @OrderColumn annotation for the gamesBlack attribute.

Implications of persisting the element’s order

Persisting the order of the elements of an association might sound like a great idea. But it requires a management effort that can slow down your write operations. Because Hibernate persists each element’s position, it has to update multiple records if you’re adding or removing any element that’s not the last one in the List.

In the previous examples, I added an element to the end of the List. That didn’t require any changes to the index of the other elements. But that changes when you add an element somewhere in the middle.

tournament = em.find(ChessTournament.class, 1L);
ChessPlayer player4 = em.find(ChessPlayer.class, 4L);
tournament.getPlayers().add(1, player4);

By adding the ChessPlayer on position 1, the index of all elements that were on position 1 or higher gets incremented by one. Hibernate then needs to update that in the database.

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 (?, ?, ?)

And the same, of course, happens if you remove any element except for the last one.

As you can see in this example, persisting the order of the List creates an overhead. Depending on the size of the List and the kind of operations you perform, this can cause severe performance problems. I, therefore, recommend sticking to Hibernate’s old List semantics and using the new handling with great care.

Conclusion

A List is supposed to be an ordered collection of elements. But by default, Hibernate doesn’t persist the order, and the database returns the elements in random order. Due to that, the initial order of the elements is lost after Hibernate fetches the List from the database.

Previous to Hibernate 6, you can avoid that by annotating your ElementCollection, one-to-many association, or the owning side of your many-to-many association with an @OrderColumn annotation. Hibernate then stores the position of each element in a separate database column.

Since Hibernate 6, you can use a new configuration parameter to change the handling of your Lists globally for all your ElementCollections and the owning sides of your many-to-many associations. To do that, you only need to set the hibernate.mapping.default_list_semantics configuration property to LIST.

When you decide to persist the order of your Lists, please keep in mind that this can slow down your write operations. Hibernate needs to manage the index of all List elements, which requires multiple update operations if you’re inserting or removing an element that’s not the last one.