The JPA Add Problem with Backlinks
Published: 2019-12-26
Updated: 2020-01-26
Web: https://fritzthecat-blog.blogspot.com/2019/12/the-jpa-add-problem-with-backlinks.html
In my recent Blog I introduced a JPA test project that enables me to experiment with entity classes. In this Blog I would like to present a solution for the JPA add()
problem. This builds on Java 1.8 and JPA 2.1. I used a H2 database for testing.
Demo Code
The add()
problem exists just for hierarchical relations where you have a backlink to the collection-owner inside the child entity. Following example classes show just the necessary parts, I left out further properties for brevity.
Owner entity:
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
@Entity
public class Team extends BaseEntity
{
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Responsibility> responsibilities = new HashSet<>();
public Set<Responsibility> getResponsibilities() {
return responsibilities;
}
public void add(Responsibility responsibility) {
responsibilities.add(responsibility);
responsibility.setTeam(this);
}
}
You find the BaseEntity
implementation in my recent Blog. The setResponsibilities()
was left out to avoid application abuse. JPA providers don't care if it is missing, they use the private field.
Child entity (where the backlink is):
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
@Entity
public class Responsibility extends BaseEntity
{
@ManyToOne(optional = false)
private Team team;
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
}
Hierarchical relations are cascading, that means deleting the team would also delete all contained responsibilities from the database. UML calls that "Composition".
API Weakness
The problem is the unsafe API.
Alice did this:
....
team.add(responsibility);
// right, add() sets the backlink correctly
....
But Bob did this:
....
team.getResponsibilities().add(responsibility);
// wrong, because the backlink is not set
....
What makes the developer use the Team.add()
method? Nothing. It is a weak API. By the way, the add-method was coded by hand.
Can we do better?
Collection Wrapper Solution
The idea is to return a collection wrapper from getResponsibilities()
that sets the backlink whenever an entity gets added, and clears it when an entity gets removed. There would be no hand-coded explicit add()
method any more, instead wrapper classes implementing java.util.Set
and java.util.List
interfaces are needed. List
is what comes in specified order and can contain duplicates, Set
does not contain duplicates and has no order, both extend Collection
.
Mind that such a solution is possible only when you have the JPA-annotations on the fields, not on the methods, so that the JPA container will not use getResponsibilities()
!
The applying source-code would look like the following:
import java.util.*;
import javax.persistence.*;
import fri.jpa.util.BacklinkSettingCollection;
@Entity
public class Team extends BaseEntity
{
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Responsibility> responsibilities = new HashSet<>();
....
public Collection<Responsibility> getResponsibilities() {
return new BacklinkSettingCollection<Responsibility,Team>(
responsibilities,
this,
(element, owner) -> element.setTeam(owner));
}
....
}
There are no more hand-coded add()
methods. The getter now returns a wrapper instead of the original collection maintained by JPA. Here the class BacklinkSettingCollection
must cover java.util.Set
. Its constructor requires the relation collection, the owner, and a function (Java 8 lambda) that sets the owner as backlink.
Following is the BacklinkSettingCollection
implementation.
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | package fri.jpa.util;
import java.util.Collection; import java.util.Iterator; import java.util.function.BiConsumer;
public class BacklinkSettingCollection<ELEMENT,OWNER> implements Collection<ELEMENT> { private final Collection<ELEMENT> relations; private final OWNER owner; private final BiConsumer<ELEMENT,OWNER> backLinkSetter; /** * @param relations the collection to maintain. * @param owner the holder object of the relations that * must be set as owner to added elements. * @param backLinkSetter the function to use for setting the * backlink, either to owner on add, or to null on remove, * e.g. <code>(team,member) -> member.setTeam(team)</code>. */ public BacklinkSettingCollection( Collection<ELEMENT> relations, OWNER owner, BiConsumer<ELEMENT,OWNER> backLinkSetter) { assert relations != null && owner != null && backLinkSetter != null; this.relations = relations; this.owner = owner; this.backLinkSetter = backLinkSetter; } @Override public int size() { return relations.size(); }
@Override public boolean isEmpty() { return relations.isEmpty(); }
@Override public boolean contains(Object o) { return relations.contains(o); }
@Override public Iterator<ELEMENT> iterator() { return new Iterator<ELEMENT>() { private Iterator<ELEMENT> delegate = relations.iterator(); private ELEMENT current; @Override public boolean hasNext() { return delegate.hasNext(); } @Override public ELEMENT next() { return current = delegate.next(); } @Override public void remove() { delegate.remove(); castAndSetNull(current); } }; }
@Override public Object[] toArray() { return relations.toArray(); }
@Override public <T> T[] toArray(T[] a) { return relations.toArray(a); }
@Override public boolean add(ELEMENT e) { setOwner(e); return relations.add(e); }
@Override public boolean remove(Object o) { castAndSetNull(o); return relations.remove(o); }
@Override public boolean containsAll(Collection<?> c) { return relations.containsAll(c); }
@Override public boolean addAll(Collection<? extends ELEMENT> c) { for (ELEMENT e : c) setOwner(e); return relations.addAll(c); }
@Override public boolean removeAll(Collection<?> c) { for (Object o : c) castAndSetNull(o); return relations.removeAll(c); }
@Override public boolean retainAll(Collection<?> c) { for (Object o : c) if (relations.contains(o) == false) castAndSetNull(o); return relations.retainAll(c); } @Override public void clear() { removeAll(relations); }
protected final void setOwner(ELEMENT e) { backLinkSetter.accept(e, owner); }
protected final void castAndSetNull(Object o) { @SuppressWarnings("unchecked") ELEMENT e = (ELEMENT) o; backLinkSetter.accept(e, null); } }
|
Yes, the Java Collection
interface has become big.
You can see that this is a fast wrapper, because most calls simply delegate to the original collection. And it is stateless, thus it can be constructed newly on every getResponsibilities()
call. The only thing this class does is set and unset the backlink whenever a child gets added or removed.
The BiConsumer
is a ready-made Java @FunctionalInterface
that covers what I need here: a function that takes two parameters and returns nothing. Its accept()
method would call the backlink setter.
Here is the according List
implementation for ordered relation collections that can contain duplicates:
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 | package fri.jpa.util;
import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.function.BiConsumer;
public class BacklinkSettingList<ELEMENT,OWNER> extends BacklinkSettingCollection<ELEMENT,OWNER> implements List<ELEMENT> { private final List<ELEMENT> relations; /** {@inheritDoc} */ public BacklinkSettingList( List<ELEMENT> relations, OWNER owner, BiConsumer<ELEMENT, OWNER> backLinkSetter) { super(relations, owner, backLinkSetter); this.relations = relations; } @Override public boolean addAll(int index, Collection<? extends ELEMENT> c) { for (ELEMENT e : c) setOwner(e); return relations.addAll(index, c); }
@Override public ELEMENT get(int index) { return relations.get(index); }
@Override public ELEMENT set(int index, ELEMENT element) { castAndSetNull(get(index)); setOwner(element); return relations.set(index, element); }
@Override public void add(int index, ELEMENT element) { setOwner(element); relations.add(index, element); }
@Override public ELEMENT remove(int index) { castAndSetNull(get(index)); return relations.remove(index); }
@Override public int indexOf(Object o) { return relations.indexOf(o); }
@Override public int lastIndexOf(Object o) { return relations.lastIndexOf(o); }
@Override public ListIterator<ELEMENT> listIterator() { return relations.listIterator(); }
@Override public ListIterator<ELEMENT> listIterator(int index) { return relations.listIterator(index); }
@Override public List<ELEMENT> subList(int fromIndex, int toIndex) { return relations.subList(fromIndex, toIndex); } }
|
Not so big any more, because it extends BacklinkSettingCollection
. Mind that both classes have their own relations
field, but as both fields are final it is impossible that they may work on different collections.
Unit Tests
The JpaTest
that I introduced in my recent Blog still succeeded after I introduced these implementations. Which doesn't prove that they are safe, so here are tests that specialize on BacklinkSettingXXX
and don't use a database.
Collection test:
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 | package fri.jpa.util;
import static org.junit.Assert.*; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import org.junit.Test;
public class BacklinkSettingCollectionTest { protected static class Member { private Team team; public void setTeam(Team team) { this.team = team; } public Team getTeam() { return team; } } protected static class Team { protected final Collection<Member> members = new ArrayList<>(); public Collection<Member> getMembers() { return new BacklinkSettingCollection<Member,Team>( members, this, (m, t) -> m.setTeam(t)); } } @Test public void shouldSetBackLinkWhenAddingAndRemoving() { final Team team = new Team(); // test add() final Member member1 = new Member(); team.getMembers().add(member1); final Member member2 = new Member(); team.getMembers().add(member2); assertNotEquals(member1, member2); assertEquals(2, team.getMembers().size()); final Iterator<Member> iterator = team.getMembers().iterator(); assertTrue(member1 == iterator.next()); // is an ordered List assertTrue(member2 == iterator.next()); // make sure the backlink has been set assertTrue(team == member1.getTeam()); assertTrue(team == member2.getTeam()); // test remove() final Member toRemove = member1; final Member toKeep = member2; team.getMembers().remove(toRemove); assertEquals(1, team.getMembers().size()); assertFalse(team.getMembers().contains(toRemove)); assertTrue(team.getMembers().contains(toKeep)); assertTrue(null == toRemove.getTeam()); assertTrue(team == toKeep.getTeam()); // test clear() is removeAll() team.getMembers().clear(); assertEquals(0, team.getMembers().size()); assertTrue(null == toKeep.getTeam()); // test removeIf() team.getMembers().add(member1); assertTrue(team == member1.getTeam()); team.getMembers().removeIf(element -> true); assertTrue(null == member1.getTeam()); // test iterator().remove() team.getMembers().add(member1); assertTrue(team == member1.getTeam()); final Iterator<Member> iterator2 = team.getMembers().iterator(); iterator2.next(); iterator2.remove(); assertTrue(null == member1.getTeam()); assertEquals(0, team.getMembers().size()); } }
|
List test:
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 | package fri.jpa.util;
import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.junit.Test;
public class BacklinkSettingListTest extends BacklinkSettingCollectionTest { protected static class Team extends BacklinkSettingCollectionTest.Team { public Collection<Member> getMembers() { return new BacklinkSettingList<Member,Team>( (List<Member>) members, this, (m, t) -> m.setTeam(t)); } } @Test public void shouldSetBackLinkWhenUsingListApi() { final Team team = new Team(); final Member member0 = new Member(); members(team).add(0, member0); final Member member1 = new Member(); members(team).add(1, member1); assertEquals(2, team.getMembers().size()); assertTrue(team == member0.getTeam()); assertTrue(team == member1.getTeam()); final Member member1Added = new Member(); final Member member2Added = new Member(); members(team).addAll(1, Arrays.asList(member1Added, member2Added)); assertTrue(member1Added == members(team).get(1)); assertTrue(member2Added == members(team).get(2)); assertTrue(team == member1Added.getTeam()); assertTrue(team == member2Added.getTeam()); final Member member2Replacer = new Member(); members(team).set(2, member2Replacer); assertTrue(null == member2Added.getTeam()); assertTrue(team == member2Replacer.getTeam()); members(team).remove(0); assertTrue(null == member0.getTeam()); } private List<Member> members(Team team) { return (List<Member>) team.getMembers(); } }
|
Protecting the Backlink Setter
The other side of the add-method is the backlink setter method responsibility.setTeam()
. Calling this without handling the collections the responsibility
is currently in, and should go to, is also illegal. On first glance this looks not easy to solve, but what about making the setter package-visible, so that it can be called only from classes that are in same package as the entity?
@Entity
public class Responsibility extends BaseEntity
{
@ManyToOne(optional = false)
private Team team;
public Team getTeam() {
return team;
}
void setTeam(Team team) {
this.team = team;
}
}
Here, the setTeam()
method is package-visible (no access-modifier), so it is accessible only for classes within the same Java-package. The restriction this solution demands is that all persistence-classes that relate to each other must be in same package.
Conclusion
I will use this solution in my further experiments with JPA entity classes. There may be other solutions for the add()
problem, but currently I stick to this one. The JPA providers ignore the property-access methods when annotations are on the fields (see AccessType.FIELD
), so why not implement entities in a bean-unlike way to make the API safer against developer mistakes?
ɔ⃝ Fritz Ritzberger, 2019-12-26