Reputation: 1933
I have two entities mapped in my application in order to model a Team
and its Members
. A Team
can have many Members
and a Member
can belong to no more than one Team
. Everything is fine about handling this concepts. The problem comes when I try to move a Member
from one existing Team
to another.
The entities are presented below, in simplified form. The very last method, transfer()
, is the one that should perform the removal of a certain Member
from its Team
and send it to another one.
@Entity
public class Member extends Person {
@ManyToOne
private Team team;
protected Member() {
super();
}
public Member(Team team, String name) {
super(name);
this.team = team;
}
// Trivial getters and setters...
public Team getTeam() {
return team;
}
protected void setTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
private long id;
private String name;
@OneToMany(mappedBy="team", cascade=CascadeType.ALL)
private List<Member> members = new ArrayList<Member>();
protected Team() {
}
public Team(String name) {
this.name = name;
}
// trivial getters and setters...
public Member addMember(String name) {
Member member = new Member(this, name);
members.add(member);
return member;
}
protected void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
public void removeMember(Member member) {
members.remove(member);
}
public Member memberByName(String memberName) {
for(Member member : members)
if(member.getName().equals(memberName))
return member;
return null;
}
public Collection<Members> getMembers() {
return Collections.unmodifiableCollection(members);
}
public void transfer(Member member, Team destination) {
members.remove(member);
destination.addMember(member);
}
}
I have this unit test code that is intended to validate the transfer service
Team teamA = teamRepository.teamById(idTeamA);
Team teamB = teamRepository.teamById(idTeamB);
Team teamC = teamRepository.teamById(idTeamC);
Member zaki = teamA.memberByName("Zaki");
Member denise = teamA.memberByName("Denise");
EntityTransaction t = teamRepository.transactionBegin();
teamA.transfer(zaki, teamB);
teamA.transferir(denise, teamC);
t.commit();
I have the following exception in the commit()
line
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: application.domain.Member
Any ideas?
I decided to perform a little test and changed the code of the transfer()
method as follows
public void transfer(Member member, Team destination) {
member.setTeam(this);
}
The result was curious: no error, but also no update on the tables. Hibernate couldn't track the update and the transfer simply didn't happen.
I decided to give it a try on the suggestion from Steve K and changed the transfer()
method to the following:
public void transfer(Member member, Team destination) {
destination.addMember(member);
members.remove(member);
}
Looking the addMember()
and removeMember()
methods (below) we see that the Team
is begin updated too.
protected void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
public void removeMember(Member member) {
members.remove(member);
}
So, the member is being added to the destination collection, its Team is being set to the destination Team
and then the member is being removed from the current collection (current Team
).
The test case was changed to
EntityTransaction t = teamRepository.transactionBegin();
teamA.transfer(zaki, teamB);
teamA.getEntityManager().refresh(teamA); // I get an exception here!!!
t.commit();
In the refresh()
line I have the following exception:
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: domain.Member
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763)
// ... many calls
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: domain.Member
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:139)
// ... many calls
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:1335)
... 28 more
It seems, after all, that transfering instances from one collection to another (that are implementing a simple aggregation) is not supported in Hibernate!
Upvotes: 4
Views: 1803
Reputation: 154080
Your mappings and data access logic are just fine.
You need to move the transaction boundary at the beginning of the code:
EntityTransaction t = teamRepository.transactionBegin();
Team teamA = teamRepository.teamById(idTeamA);
...
teamA.transferir(denise, teamC);
t.commit();
But the exception you got it's not from the code you've shown us:
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: application.domain.Member
There might be a pending persist action being queued up and only translated during flush time (commit time in your case). So you need to activate the SQL logs being executed and try finding out the detached entity.
Upvotes: 3
Reputation: 4921
The transfer()
method is completely redundant. All you should need to do is call member.setTeam(destination)
and Hibernate will update the collections accordingly.
Your Update's code:
member.setTeam(this);
does nothing. It should be
member.setTeam(destination);
You're seeing no updates because you're not changing the value. The member's already a member of this
Team
.
Try just changing your transfer() method to this:
public void transfer(Member member, Team destination){
member.setTeam(destination);
}
After you've moved around all the team members you want to move, you can commit or flush the transaction before moving on to the next unit of work. Moving the items around in the local lists is not necessary unless your units of work are badly defined. Transfer your people, then commit. Then you're ready for the next bit of processing. If you need to do so in the middle of a transaction, then call entityManager.flush()
which will execute the currently queued updates but still allow you to later commit or rollback. Flushing in this manner is an anti-pattern, though. If you need to know which team a member is a part of during your processing, ask the member, not the team (which is much more efficient anyway).
Upvotes: 0
Reputation: 24433
There are a couple of things that are not clear. First one is how do you handle hibernate session in this test? Is it active throughout the whole test? Second, does teamRepository.teamById(idTeamA)
create also members? Because Member zaki = teamA.memberByName("Zaki")
will return null
if Zaki
isn't created.
My setup would be something like this:
@Before
method, initial data is created (teamA (with Zaki and Denise), teamB and teamC)In test method, begin transaction and get those entities by id
teamRepository.transactionBegin();
Team teamA = teamRepository.teamById(idTeamA); // does this create a team, or returns an existing one? here, it should only return existing team
... // get all teams
Member zaki = teamA.memberByName("Zaki");
... // get all members
Transfer members
teamA.transfer(zaki, teamB);
teamA.transfer(denise, teamC);
Commit transaction
t.commit();
Upvotes: 0
Reputation: 116
Moving Member
from one list to other is not going to change the team
value. Better idea would be to directly change team
from A to B in a Member
entity.
Upvotes: 1