One to One Shared Primary Key is Eagerly Fetched
Every so often Hibernates presents some peculiarity that, at first observance, doesn't make much sense. However, once you investigate the peculiarity thoroughly, you see why hibernate behaves the way it does.
An instance occurs when mapping a one-to-one relationship with a shared primary key. In certain instances, even if this mapping is declared to fetch lazily, hibernate will eagerly fetch the association. The reason is because due to the nature of a shared primary key, hibernate does not know whether to initialize the association to null or not without actually joining against the associated table.
Explore through an Example
Let's set up an example to further explain the situation. Let's say we want to map a one-to-one association with a shared primary key for two basic entities: a Passenger and a AirlineTicket. A Passenger can only have one Airline ticket, and an Airline ticket can only have one passenger. For our example, the relationship will be bidirectional from Passenger to AirlineTicket.
We can map the objects with JPA and hibernate with the code snippets below. Note that we have marked the association from Passenger to AirlineTicket as LAZY.
....
@Entity
@Table(name = "PASSENGER")
public class Passenger {
@Id
@GeneratedValue
@Column(name = "PASSENGER_ID")
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "BIRTHDATE")
private int age;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "passenger")
private AirlineTicket airlineTicket;
...
}
...
@Entity
@Table(name = "AIRLINE_TICKET")
@org.hibernate.annotations.GenericGenerator(name="passenger-primarykey", strategy="foreign",
parameters={@org.hibernate.annotations.Parameter(name="property", value="passenger")
})
public class AirlineTicket {
@Id
@GeneratedValue(generator = "passenger-primarykey")
@Column(name = "PASSENGER_ID")
private Long id;
@Column(name = "SEAT_NUMBER")
private int seatNumber;
@Column(name = "CLASS")
private String clazz;
@OneToOne
@PrimaryKeyJoinColumn
private Passenger passenger;
public AirlineTicket() {
}
...
}
However, when we load the entity with a entityManager.find(..), hibernate issues a join in the generated sql to eagerly load the AirlineTicket! If the Passenger entity was loaded with a ejql statement such as "from Passenger", hibernate will issue two separate sql statements back to back: one to load the passenger, and then another to load the airline ticket (which essentially is an eager fetch.)
Why does this peculiarity occur? When hibernate loads the Passenger object, it has to initialize its attributes. AirlineTicket is an association which is mapped with a shared primary key. Therefore, in order to find out whether the AirlineTicket is null or not, hibernate must issue a join to the AIRLINE_TICKET table to check if a row exists with the same primary key as the Passenger object. If hibernate abstained from issuing a join to AIRLINE_TICKET and proceeded to instantiate a proxy for AirlineTicket, the Passnger object would contain a AirlineTicket, even if there was no real association in the database.
Solution
In order to take advantage of lazy loading when you have a shared primary key one-to-one relationship, you can use the optional=false setting on the relationship. For example, the AirlineTicket reference in the Passenger object would have optional=false. This conveys to hibernate that the there will always be an AirlineTicket for a Passenger, thus it can create a proxy and it is guarenteed to have a corresponding object.
Design Considerations
So what are the situations to use a shared primary key in a one-to-one? This begs the question of when is it best to use a one-to-one relationship? One-to-One relationships are modeled in the domain when one object instance has an exclusive relationship with another object instance. Simple examples would be [Person, Heart], [Husband, Wife], [HeadCoach, NflTeam]. (Of course, you could make silly arguments to debunk this, but you get the drift).
The easiest mapping for one-to-one relationship is to have a foreign kep from the owning entity to the other object. In our Passenger example, the PASSENGER table would have a foreign key column AIRLINE_ID. This approach has no peculiarities with lazy loading, as hibernate can figure out if the association exists without joining to another table. All it has to do is check the foreign key for null.
A shared primary key for one-to-one mapping is appropriate only if the association is non-optional. Meaning each end of the association has to exist. If one side exists, so does the other. If you map without this symbiotic relationship, you will run into the peculiarities above and possibly have to eager fetch for no reason. This could lead to performance issues. Shared primary keys save a column on the database, but the mapping is a little more complicated and peculiarities exist. Hardware is cheap, knowledge is expensive. Consider this approach carefully.
References:
Thanks for this article, it helped me a lot. However on my side, I have bi-directional OneToOne where one direction is nullable. So the optional=false caused an exception when saving the parent.
ReplyDeleteTo make things work, I had to use:
@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "MY_COLUMN", insertable = false, updatable = false)
If there were any cascade operations configured on this relation, I guess I would lose them.
Thank you very much. You made my day. I can sleep at 1am now instead of 3am. I owe you one.
ReplyDeleteInteresting Article
ReplyDeleteSpring Hibernate Online Training | Hibernate Training in Chennai
Hibernate Online Training | Java Online Training | Java EE Online Training
Thanks for sharing this informative content , Great work
ReplyDeleteLeanpitch provides online training inScrum Master during this lockdown period everyone can use it wisely.
CSM online certification
Articles can explore the influence of technology on different aspects of society Nord VPN Offer from communication to healthcare and beyond in the field of sales.
ReplyDelete