Mar 23, 2019

MappedBy @OrderColumn with Hibernate

If you have been using Hibernate for some time you should know that the best way to map a one-to-many relationship is through a bidirectional @OneToMany association where the mappedBy attribute is set. It is well explained in this article:

The best way to map a @OneToMany relationship with JPA and Hibernate

See also the Hibernate ORM User Guide.

Now, in honour of Saint Thomas, we are going to verify ourselves if that's true. Furthermore we will investigate what happens when we add the @OrderColumn annotation.

Sample project

Source code

The sample project is based on Hibernate 5.4.1.Final and Postgres. There are two tables, master and slave, with auto-incrementing primary keys. The slave table has a foreign key on id_master and a column named index which will be used later with @OrderColumn.


CREATE TABLE master
(
    id          serial     NOT NULL,
    description text,
    CONSTRAINT master_pkey PRIMARY KEY (id)
)

CREATE TABLE slave
(
    id          serial     NOT NULL,
    description text,
    id_master   integer,
    index       integer,
    CONSTRAINT slave_pkey PRIMARY KEY (id),
    CONSTRAINT slave_master_fk FOREIGN KEY (id_master)
        REFERENCES master (id)
)

Unidirectional association

First case. We have two entities, Master and Slave. Master has a list of slaves mapped with @OneToMany and @JoinColumn annotations, the mappedBy attribute is not used. Slave has no association towards Master.


@Entity(name = "MasterNoOrderUni")
@Table(name = "master")
public class Master
{
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private int id = 0;
 
 private String description = "";

 @OneToMany(
   cascade = CascadeType.ALL,
   orphanRemoval = true)
 @JoinColumn(name = "id_master")
 private List<Slave> slaves = null;

// ...

 public void addSlave(String description)
 {
  Slave slave = new Slave(this, description);
  slaves.add(slave);
 }

// ...
}

@Entity(name = "SlaveNoOrderUni")
@Table(name = "slave")
public class Slave
{
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private int id = 0;
 
 private String description = "";

// ...
}

Then, this is the code to persist a new Master and three Slave instances. I hope you will not be offended by what you are going to read, JPA providers are hellish tools.


 app.ms.noOrderColumn.unidirectional.Master master = 
  new app.ms.noOrderColumn.unidirectional.Master("Devil");
 master.addSlave("Devil's slave A");
 master.addSlave("Devil's slave B");
 master.addSlave("Devil's slave C");
 
 EntityManager em = emf.createEntityManager();
 em.getTransaction().begin();
 
 em.persist(master);
 
 em.getTransaction().commit();
 em.close();

Revised console output follows.


    insert into master
        (description) 
    values
        ('Devil')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 13


    insert into slave
        (description) 
    values
        ('Devil''s slave A')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 37

    insert into slave
        (description) 
    values
        ('Devil''s slave B')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 38

    insert into slave
        (description) 
    values
        ('Devil''s slave C')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 39


    update slave set
        id_master=13 
    where
        id=37

    update slave set
        id_master=13 
    where
        id=38

    update slave set
        id_master=13 
    where
        id=39

Clearly the three update statements could have been avoided. The insert statements are executed on the persist instruction, the update statements are executed on the commit instruction. It is all implementation-related.

Bidirectional association

Second case. A @ManyToOne and a @JoinColumn have been added to Slave, so now a bidirectional association is in place.

This is not a bidirectional association in JPA terms (see SR 338: Java Persistence 2.2 - page 44, 2.9 Entity Relationship) but in this context we need to make a distinction, that is: Bidirectional association vs Bidirectional association with mappedBy.


@Entity(name = "MasterNoOrderBi")
@Table(name = "master")
public class Master
{
// ...
 
 @OneToMany(
   cascade = CascadeType.ALL,
   orphanRemoval = true)
 @JoinColumn(name = "id_master")
 private List<Slave> slaves = null;

// ...
}

@Entity(name = "SlaveNoOrderBi")
@Table(name = "slave")
public class Slave
{
// ...
 
 @ManyToOne
 @JoinColumn(name = "id_master")
 private Master master = null;

// ...
}

Here there is only a German difference.


 app.ms.orderColumn.bidirectional.Master master = 
  new app.ms.orderColumn.bidirectional.Master("Teufel");
 master.addSlave("Teufel's slave A");
 master.addSlave("Teufel's slave B");
 master.addSlave("Teufel's slave C");
 
// ...

The output.


    insert into master
        (description) 
    values
        ('Teufel')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 14


    insert into slave
        (description, id_master) 
    values
        ('Teufel''s slave A', 14)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 40

    insert into slave
        (description, id_master) 
    values
        ('Teufel''s slave B', 14)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 41

    insert into slave
        (description, id_master) 
    values
        ('Teufel''s slave C', 14)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 42


    update slave set
        id_master=14 
    where
        id=40

    update slave set
        id_master=14 
    where
        id=41

    update slave set
        id_master=14 
    where
        id=42

We still have inefficiency. d_master is set immediately with the insert statements but the same identical result could have been obtained in the previous case by setting @JoinColumn with nullable to false.


 @JoinColumn(name = "id_master", nullable = false)

So, effectively, we can consider this case like a unidirectional association.

Bidirectional association with mappedBy

Third case. This time Master has a @OneToMany annotation and the mappedBy attribute is there in all its glory, moreover no @JoinColumn is present because the field that owns the relationship is in Slave.


@Entity(name = "MasterNoOrderBiMappedBy")
@Table(name = "master")
public class Master
{
// ...
 
 @OneToMany(
   mappedBy = "master",
   cascade = CascadeType.ALL,
   orphanRemoval = true)
 private List<Slave> slaves = null;

// ...
}

@Entity(name = "SlaveNoOrderBiMappedBy")
@Table(name = "slave")
public class Slave
{
// ...
 
 @ManyToOne
 @JoinColumn(name = "id_master")
 private Master master = null;

// ...
}

Here there is only a Spanish difference.


 app.ms.orderColumn.bidirectional.mappedBy.Master master = 
  new app.ms.orderColumn.bidirectional.mappedBy.Master("Diablo");
 master.addSlave("Diablo's slave A");
 master.addSlave("Diablo's slave B");
 master.addSlave("Diablo's slave C");
 
// ...

And the result is shorter than before.


    insert into master
        (description) 
    values
        ('Diablo')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 15

    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave A', 15)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 43

    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave B', 15)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 44

    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave C', 15)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 45

The three update statements previously seen have disappeared, this is what we would have expected from the start. It is confirmed, the bidirectional association with mappedBy is the best way to map a @OneToMany relationship (with Hibernate, for now).

@OrderColumn

But what happens when @OrderColumn is used to maintain order between the slaves?

Several past Hibernate versions were having problems with @OrderColumn (or the deprecated @IndexColumn) when it was combined with @OneToMany and mappedBy (e.g., see here, here and here).

Now we have to put the finger in this wound also, adding @OrderColumn to the previous mappings.


// ...
// Unidirectional

 @OneToMany(
   cascade = CascadeType.ALL,
   orphanRemoval = true)
 @JoinColumn(name = "id_master")
 @OrderColumn(name = "index", nullable = false)
 private List<Slave> slaves = null;

// ...
// Bidirectional with mappedBy

 @OneToMany(
   mappedBy = "master",
   cascade = CascadeType.ALL,
   orphanRemoval = true)
 @OrderColumn(name = "index", nullable = false)
 private List<Slave> slaves = null;

// ...

Output for the unidirectional association.


/* 
    Unidirectional
*/

    insert into master
        (description) 
    values
        ('Devil')

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 16


    insert into slave
        (description) 
    values
        ('Devil''s slave A', 16)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 46

    insert into slave
        (description, id_master) 
    values
        ('Devil''s slave B', 16)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 47

    insert into slave
        (description, id_master) 
    values
        ('Devil''s slave C', 16)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 48


    update slave set
        id_master=16,
        index=0 
    where
        id=46

    update slave set
        id_master=16,
        index=1 
    where
        id=47

    update slave set
        id_master=16,
        index=2 
    where
        id=47

Output for the bidirectional association with mappedBy.


/*
    Bidirectional with mappedBy
*/

    insert into master
        (description) 
    values
        (Diablo)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 18


    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave A', 18)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 52

    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave B', 18)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 53

    insert into slave
        (description, id_master) 
    values
        ('Diablo''s slave C', 18)

-- DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 54


    update slave set
        index=0 
    where
        id=52

    update slave set
        index=1 
    where
        id=53

    update slave set
        index=2 
    where
        id=54

It is a step back. When @OrderColumn enters the scene even a bidirectional association with mappedBy becomes plagued by those ugly update statements. Does it means that the two types of association are now equivalent? Not really. Let's consider another situation.


 // ...

 app.ms.orderColumn.unidirectional.Master master = 
  em.find(app.ms.orderColumn.unidirectional.Master.class, 16);
 master.removeSlave("Devil's slave B");

 // ...

 app.ms.orderColumn.bidirectional.mappedBy.Master master = 
  em.find(app.ms.orderColumn.bidirectional.mappedBy.Master.class, 18);
 master.removeSlave("Diablo's slave B");

 // ...

We are putting an end to the sufferings of slaves B.


/* 
    Unidirectional, remove B
*/

    update slave set
        id_master=null,
        index=null 
    where
        id_master=16 
        and id=48

    update slave set
        id_master=null,
        index=null 
    where
        id_master=16 
        and id=47

    update slave set
        id_master=16,
        index=1 
    where
        id=48

    delete from slave 
    where
        id=47

The first two updates are useless. Hibernate is setting the id_master and index columns to null for all those elements that it has effectively to update or delete. It is applying a sort of punishment.


/*
    Bidirectional with mappedBy, remove B
*/

    update slave set
        index=0 
    where
        id=52

    update slave set
        index=1 
    where
        id=54

    delete from slave 
    where
        id=53

The first update is useless. Hibernate is updating the index column for all the remaining elements.

At first sight it could be said that the bidirectional association with mappedBy is still the best because there are less update statements, but what would have happened removing slave C instead of B?


/* 
    Unidirectional, remove C
*/

    update slave set
        id_master=null,
        index=null 
    where
        id_master=16 
        and id=48

    delete from slave 
    where
        id=48


/*
    Bidirectional with mappedBy, remove C
*/

    update slave set
        index=0 
    where
        id=52

    update slave set
        index=1 
    where
        id=53

    delete from slave 
    where
        id=54

Conclusion

The best way to map a @OneToMany relationship with Hibernate is through a bidirectional association (with mappedBy), but this is true if @OrderColumn is not being used. When @OrderColumn is specified then the number of elements involved and the operations performed must be considered. Other JPA providers (precisely EclipseLink and OpenJPA) are doing better in some of the cases analyzed here, so let's keep an eye on Hibernate 6 or 666.

No comments:

Post a Comment

Note: Comments are moderated.