If you want to refer to a parent entity through an ID, not through an object, you may get problems using JPA. The according use case may be that you want to serialize the child entity, but the parent should be contained just by ID, not as an object with possibly other references.
Following is a bidirectional (hierarchical) one-to-many relation (BaseEntity
is in chapter "Source Code"). Team
is the parent, Meeting
is the child.
@Entity
public class Team extends BaseEntity
{
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Meeting> meetings = new ArrayList<>();
....
}
@Entity
public class Meeting extends BaseEntity
{
@ManyToOne(optional = false)
private Team team;
....
}
The same with just the ID as backlink (UUID
):
@Entity
public class Team extends BaseEntity
{
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "TEAM_FK")
private List<Meeting> meetings = new ArrayList<>();
....
}
@Entity
public class Meeting extends BaseEntity
{
@Column(name = "TEAM_FK")
private UUID teamId;
....
}
This may look smooth, but it seems to NOT work with EclipseLink, although it works with Hibernate (the two most prominent JPA providers).
EclipseLink throws this exception:
Exception [EclipseLink-3002] (Eclipse Persistence Services - 2.7.5.v20191016-ea124dd158): org.eclipse.persistence.exceptions.ConversionException
The object [[B@1e1b061], of class [class [B], from mapping [org.eclipse.persistence.mappings.DirectToFieldMapping[teamId-->MEETINGX.TEAM_FK]] with descriptor [RelationalDescriptor(fri.jpa.example.idref.MeetingX --> [DatabaseTable(MEETINGX)])], could not be converted to [class java.util.UUID].
at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:161)
Once again, in words:
Obviously EclipseLink wants to convert the Meeting
object to an UUID
on cascading save of the Team
. Writing a Converter is not a solution here, because converters are not allowed on relationship attributes, and they serve a different purpose.
Here is the base-entity implementation:
import java.util.UUID;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class BaseEntity
{
@Id private UUID id = UUID.randomUUID();
public final UUID getId() {
return id;
}
@Override
public final boolean equals(Object o) {
if (this == o) // performance optimization
return true;
if (o == null || getClass() != o.getClass()) // exclude aliens
return false; // and one-to-one entities with same id
final BaseEntity other = (BaseEntity) o;
return id.equals(((BaseEntity) o).id); // delegate equality to id
}
@Override
public final int hashCode() {
return id.hashCode();
}
}
Following are the entity classes to reproduce the problem:
import java.util.*;
import javax.persistence.*;
import fri.jpa.util.BacklinkSettingList;
@Entity
public class TeamX extends BaseEntity
{
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "TEAM_FK") // required, refers to the field in class "Meeting",
// without this annotation, an m:n relation would be built instead of 1:n
private List<MeetingX> meetings = new ArrayList<>();
public List<MeetingX> getMeetings() {
return new BacklinkSettingList<MeetingX,TeamX>(
meetings,
this,
(element, owner) -> element.setTeamId(owner.getId()));
}
}
For source code of the BacklinkSettingList
please go to my recent Blog about JPA backlinks.
package fri.jpa.example.idref;
import java.util.*;
import javax.persistence.*;
@Entity
public class MeetingX extends BaseEntity
{
@Column(name = "TEAM_FK") // without this annotation,
// there would be both "teamId" and "TEAM_FK" columns in database
private UUID teamId;
private Date dateAndTime;
public UUID getTeamId() {
return teamId;
}
public void setTeamId(UUID teamId) {
this.teamId = teamId;
}
public Date getDateAndTime() {
return dateAndTime;
}
public void setDateAndTime(Date dateAndTime) {
this.dateAndTime = dateAndTime;
}
}
Please read the inline comments to follow the test. Read this Blog to find out how to run this abstract test with both EclipseLink and Hibernate.
1 | import static org.junit.Assert.*; |
Finding a solution for this small problem within the JPA specification seems to be impossible due to its size and complexity. EclipseLink is the JPA reference implementation, thus we should not use ids as backlinks instead of objects.
ɔ⃝ Fritz Ritzberger, 2020-01-19