Blog-Archiv

Sonntag, 19. Januar 2020

JPA Backlink as ID not Object

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: