Friday, July 15, 2011

Many To Many Problems

This week I've been struggling with a many to many mapping where the join table has extra columns and the lower side of the many to many should be deleted once all the top side holders are removed.  First the model..

(Gliffy account expired so no pictures.)
  • Folder
    • Message
      • Contents
The basic premise is that a folder can contain many messages.  Messages contain 1 content, but 1 content can be referenced by many messages.  A message represents content in a user's folder.

The test case is to send a message to five people's inbox folder.  So each folder will have a Message and each message will reference the same Content.  So I would expect 5 folders, 5 messages and 1 content in the database.  If the first 4 users delete the message I would expect the 5 folders, 1 message and 1 content remaining.  When the last of the messages are deleted, the content should be deleted too.

We started by mapping the Folder's set of messages as ManyToOne.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "folder", orphanRemoval=true)
private Set messages = new HashSet();

The orphanRemoval is the equivalent of hibernate's cascade type DELETE_ORPHAN. Then we map the Message class back to the Folder as a many to one.

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "FOLDER_ID", nullable = false)
private Folder folder;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "CONTENT_ID", nullable = false)
private Content content;


Lastly we map the Contents to Message as a OneToMany and we effectively have a ManyToMany with extra attributes on the Message join table.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "content", orphanRemoval=true)
private Set messages = new HashSet();


Now we add a few convenience methods to update both sides of the one to many relationships.  When we have mappedBy in the mapping it is equivalent to inverse=true which means the Message is the owning side of the relationships.  Therefore, when we add to the folder.messages collection, we also need to set the message.folder.  Likewise on the content.messages and message.content.

public void addMesage(Message message) {
this.messages.add(message);
message.setFolder(this);
}



Everything worked pretty well when adding although the Content didn't seem to persist automatically and we would get a transient entity error.  The create mesage looks like this

public void createMessaage(List to, Content content) {
for (Folder folder : to) {
Message message = new Message();
folder.addMessage(message);
content.addMessage(message);
}
save();
}


Delete works pretty straightforward also.  Simply remove the message from the folder and the content.   Unfortunately what we encountered were a lot of re-saved entity issues and content either being removed prematurely (the entire graph deleted on first message delete) or never at all (content not deleted after last message reference removed).

My solution has been to make the delete return a boolean as to whether the Content needs removed and then issue session.delete(content) on it.  I don't like it, but it works.

No comments:

Post a Comment