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).
The Problem
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:
- The object ... from mapping ... teamId-->MEETINGX.TEAM_FK ... with descriptor ... MeetingX ... could not be converted to [class java.util.UUID].
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.
Source Code
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; } }
Unit Test
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import static org.junit.Assert.*; import java.util.*; import java.util.function.Consumer; import javax.persistence.*; import org.junit.*; public abstract class JpaTest { private EntityManager em; /** @return the name of the persistence-unit to use for all tests. */ protected abstract String getPersistenceUnitName(); @Before public void setUp() { em = Persistence.createEntityManagerFactory(getPersistenceUnitName()).createEntityManager(); } @After public void tearDown() { for (TeamX team : findTeams()) transactional(em::remove, team); for (MeetingX meeting : findMeetings()) transactional(em::remove, meeting); } @Test public void joinColumnsForFoundationLocationAndMeetings() { // build test data in memory final TeamX team = new TeamX(); // add meetings to the team final Date firstMeetingDate = new Calendar.Builder().setDate(2018, 11, 31).build().getTime(); final MeetingX firstMeeting = newMeeting(firstMeetingDate); team.getMeetings().add(firstMeeting); final Date secondMeetingDate = new Calendar.Builder().setDate(2019, 0, 1).build().getTime(); final MeetingX secondMeeting = newMeeting(secondMeetingDate); team.getMeetings().add(secondMeeting); // store the built team to persistence transactional(em::persist, team); // check that cascading also stored meetings final List<MeetingX> persistentMeetings = findMeetings(); assertEquals(2, persistentMeetings.size()); final MeetingX persistentFirstMeeting = persistentMeetings.stream() .filter(m -> m.equals(firstMeeting)) .findFirst().get(); assertNotNull(persistentFirstMeeting); assertEquals(firstMeetingDate, persistentFirstMeeting.getDateAndTime()); final MeetingX persistentSecondMeeting = persistentMeetings.stream() .filter(m -> m.equals(secondMeeting)) .findFirst().get(); assertNotNull(persistentSecondMeeting); assertEquals(secondMeetingDate, persistentSecondMeeting.getDateAndTime()); // check that the persisted team is same as the one in memory final TeamX persistentTeam = findTeams().get(0); assertEquals(team, persistentTeam); assertEquals(persistentMeetings.size(), persistentTeam.getMeetings().size()); for (MeetingX meeting : persistentTeam.getMeetings()) { assertTrue(persistentMeetings.contains(meeting)); assertEquals(team.getId(), meeting.getTeamId()); } // remove the team transactional(em::remove, team); // check that cascading removed also meetings and locations assertEquals(0, findTeams().size()); assertEquals(0, findMeetings().size()); } private <P> P transactional(Consumer<P> entityManagerFunction, P parameter) { final EntityTransaction transaction = em.getTransaction(); try { transaction.begin(); entityManagerFunction.accept(parameter); transaction.commit(); return parameter; } catch (Throwable th) { th.printStackTrace(); if (transaction.isActive()) transaction.rollback(); throw th; } } private MeetingX newMeeting(Date dateAndTime) { final MeetingX meeting = new MeetingX(); meeting.setDateAndTime(dateAndTime); return meeting; } private List<MeetingX> findMeetings() { return findAll(MeetingX.class, em); } private List<TeamX> findTeams() { return findAll(TeamX.class, em); } private <T> List<T> findAll(Class<T> persistenceClass, EntityManager em) { return em .createQuery( "select x from "+persistenceClass.getName()+" x", persistenceClass) .getResultList(); } } |
Conclusion
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.
Keine Kommentare:
Kommentar veröffentlichen