4

I have a pair of entities, call them "Ticket" and "TicketComment".

Ticket has this property:

@OneToMany(mappedBy="ticket", cascade = CascadeType.ALL)
@OrderBy("date")
private List<TicketComment> comments = new ArrayList<>();

And, TicketComment has this:

@ManyToOne
@JoinColumn(name="TKT_ID")
@NotNull
private Ticket ticket;

The code I'm using should, I think, add a single new comment. However, it seems to add the new comment to the database twice:

Ticket ticket = ticketRepo.findOne(id);

TicketComment newComment = new TicketComment();
// ...
newComment.setTicket(ticket);
ticket.getComments().add(newComment);

ticketRepo.save(ticket);

I think I've been able to do this successfully with similar entities before without the duplicate child entities... what might I be missing?


Update: a workaround seems to be:

  1. Create another JpaRepository for "TicketComment"
  2. Save any other updates to Ticket without creating/adding the comment using ticketRepo.
  3. Create the new comment, linked up with the ticket (newComment.setTicket(ticket);), and save it with a ticketCommentRepo.

So, there's two distinct saves, one which should only affect the parent, and one which should only affect the child.

I'd like to bring it down to a single save, but I'm not sure that's possible?


Update 2: clarification of the observed symptoms.

What happens if I try to simply save my parent "Ticket" entity with the new "TicketComment" added is that I see two distinct records created in the database, with different primary keys, but which have identical comment text, and identical, or nearly so, timestamps.

So, for example, say I posted a comment like "Looks good". Even though I confirmed in a debugger that there's only a single "Looks good" showing in my ticket.getComments()... after I load the ticket's details page again, I see something like:

me @ 2015-09-11 23:16:58: Looks good

me @ 2015-09-11 23:16:59: Looks good

When I looked at some debug logs of the SQL statements being prepared, I saw two identical looking INSERT statements being called for my TicketComments table, followed by a single UPDATE for my Tickets table...

pioto
  • 2,472
  • 23
  • 37
  • Can you share your ticketRepo findOne and save methods? – bedrin Aug 10 '15 at 20:14
  • Those are generated by Spring Data JPA. – pioto Aug 10 '15 at 20:16
  • Do you save newComment after then? – Youans Aug 10 '15 at 22:00
  • Are you using Hibernate as the JPA provider? – manish Aug 11 '15 at 15:10
  • @manish Yes, Hibernate is my JPA provider. – pioto Aug 12 '15 at 03:11
  • 2
    Then, this is a duplicate of [an earlier post](http://stackoverflow.com/questions/7903800/hibernate-inserts-duplicates-into-a-onetomany-collection). You have been hit by a Hibernate bug. The linked post has multiple workarounds, including, saving the child entity before adding it to the parent entity, use of `Set` instead of `List`, etc. – manish Aug 12 '15 at 10:15

2 Answers2

2

Really I do not understand what you mean with "add the new comment to the database twice". Are you seeing two registries with same PK?

I think you should use Set interface instead of List if you are expecting that each comment appears once in the ticket's collection. Anyway, I will explains what I know about the behaivor expected for that snippet of code by parts.

Ticket ticket = ticketRepo.findOne(id);
TicketComment newComment = new TicketComment();

The ticket was already persisted? doesn't matter because you will save() it. And you create a new comment.

newComment.setTicket(ticket);

This is necessary due to the owner side of the association is in TicketComment entity, and to keep object model consistently (in some cases may not be needed).

ticket.getComments().add(newComment);

As you set cascade=ALL on ticket.comments collection, all the actions (save, update, etc.) triggered on ticket will be propagated to all the entity objects in the collection.

ticketRepo.save(ticket);

When you pass ticket to save() method, the action is propagated to newComment entity you just add. The newComment will be passed to saveOrUpdate() and then persisted along with its relationship with ticket.

Guillermo
  • 1,523
  • 9
  • 19
  • Conceptually, a Set doesn't make as much sense to me... first, deciding upon the implementation of `equals()` is odd... technically, someone /could/ post the same comment twice. And I'm having Spring Data JPA populate the comment time for me. Plus, these have a logical order to apply (the time the comment was made), can JPA do that for Sets? (As in, does it support using a [`SortedSet`](http://docs.oracle.com/javase/8/docs/api/java/util/SortedSet.html) instead of a regular [`Set`](http://docs.oracle.com/javase/8/docs/api/java/util/Set.html)? – pioto Sep 25 '15 at 16:21
  • If I am understanding what you said. Why Set makes sense? First because It is a workaround for the bug (https://hibernate.atlassian.net/browse/HHH-6776) that @manish commented (you are suffering it). Secondly, due to you represent the association with a field/column in the table of `TicketComment`, each `TicketComment` can belong to one `Ticket.comments` collection and appears just once in it (like a set). List conceptually allows repeat the appearance of an object but you can't save such a state with your representation. To be clear, each row of `TicketComment` is a unique comment (has a id – Guillermo Sep 26 '15 at 06:44
  • ...To be clear, each row of the table is a unique comment. It has unique primary key that identifies it, doesn't matter if another row has the same *comment text*, they are different. You know the reality and what applies but same text doesn't means it is the same comment. For example pay attention on what stackoverflow does with my next comment – Guillermo Sep 26 '15 at 06:54
  • ...To be clear, each row of the table is a unique comment. It has unique primary key that identifies it, doesn't matter if another row has the same *comment text*, they are different. You know the reality and what applies but same text doesn't means it is the same comment. For example pay attention on what stackoverflow does with my next comment – Guillermo Sep 26 '15 at 06:54
  • Yes, but these IDs are being generated by the database, so they do not exist in the child objects until they have been persisted. The point is also, you presumably submitted the same text twice. However, I've only submitted it once. I agree that this is an instance of HHH-6776, but I don't agree that using a `Set` is the correct workaround in my case. I think I'll just have to create some trivial [`JpaRepository`](http://docs.spring.io/spring-data/jpa/docs/1.9.0.RELEASE/api/org/springframework/data/jpa/repository/JpaRepository.html) interfaces for my child elements. – pioto Sep 26 '15 at 11:24
  • You are using `List` but the database model you choosed is prepared to save states of a `Set` collection so to me, based on your reasons, doesn't justify to use List. But everything is up to your requirements and desire :) Go ahead! – Guillermo Sep 26 '15 at 11:59
0

You don't need to specify ticket in newComment Just remove the following line

newComment.setTicket(ticket);

And that would work since cascade will automatically assign ticket to newComment and vise versa you actually double assign and that may be the cause of duplication.

Youans
  • 4,801
  • 1
  • 31
  • 57
  • 1
    The **owner** side of the association is in TicketComment (@ManyToOne) so that line can't be removed in order to persist the relationship. The cascade just propagate the save action. – Guillermo Aug 10 '15 at 22:35
  • Yes I know the ``TicketComment`` is the owner but that make it the child too on that relation so when you persist the parent children will be persisted if ``CascadeType.PERSIST`` or ``CascadeType.ALL`` is applied to children – Youans Aug 10 '15 at 22:45
  • You said *"cascade will automatically assign ticket to newComment "* but cascade won't do this because is a bi-directional association. And remove that line is against the JPA specification. – Guillermo Aug 10 '15 at 23:01