2

I have a situation where I need to persist a hierarchy of items:

A Call has a Quote and a Quote has several QuoteFragments. The QuoteFragments are 'owned' by the parent Quote.

The Quote already exists in the database, but the Call and a QuoteFrament are new (there may be other QuoteFragments in the Quote that already exist)

class Call {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
  @JoinColumn(name="contract_quote_id", nullable=true)
  protected Quote quote;

  @OneToOne(fetch=FetchType.LAZY, cascade={CascadeType.REFRESH})
  @JoinColumn(name="fragment_id", nullable=true)
  protected QuoteFragment fragment;
}

class Quote {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="customer_call_id", nullable=false)
  protected Call customerCall;

  @OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, mappedBy="parent")
  protected Set<QuoteFragment> fragments = new HashSet<>();
}

class QuoteFragment {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @ManyToOne(fetch=FetchType.EAGER)
  @JoinColumn(name="parent_id", nullable=false)
  protected Quote parent;

  @Column(name="valid_until", nullable=false)
  @Temporal(TemporalType.DATE)
  protected Date validUntil;
}

The onetoone/manytoone on the quote/call relationship is because when the quote is first persisted it is associated with that first call. Subsequent calls may refer to the same quote but the quote will always refer to the first call.

My problem: I can't find a way using cascades or separate persist/merge actions to stop extra QuoteFragment copies being persisted to the database.

Here is my current code:

  Quote quote = getExistingQuote( ... );
  QuoteFragment fragment = new QuoteFragment();
  fragment.setParent( quote );
  quote.addFragment( fragment );

  Call call = new Call();
  call.setQuote( quote );
  call.setQuoteFragment( fragment );

  // doing other config stuff

  // later on at the persist stage..
  Quote existingQuote = call.getQuote();
  call.setQuote( null );

  // if attaching a fragment, remove before inserting or it will be inserted twice (once by the call and once by the parent)
  QuoteFragment newfrag = call.getQuoteFragment();
  call.setQuoteFragment( null );

  // This bit should insert the Call record but not the quote/fragment
  call = entityManager.persist( call );

  call.setQuote( existingQuote );
  // This should update any quote details, including persisting the fragment
  entityManager.merge( call );

  if( newfrag != null ) {
    // a new fragment was created - need to set this back to the call and update it (the first update will have created it within the parent quote)
    call.setQuoteFragment( newfrag );
    entityManager.merge( call );
  }

I thought by doing this it would only persist the fragment once, but it creates 3 records!

Can anyone help me wire all these up correctly (ideally with a single db write rather than 3) without creating the duplicates?

I'm using JPA 2.1 with Hibernate 4.3.11

Thanks in advance.

fancyplants
  • 1,577
  • 3
  • 14
  • 25

1 Answers1

2

The confusion here is regarding the roles of persist and merge. Take a look at this excellent answer for more info. I'll copy the important points here, with credit to user Josep Pandero, in case it ever should vanish:

persist:

  • Insert a new register to the database
  • Attach the object to the entity manager.

merge:

  • Find an attached object with the same id and update it.
  • If exists update and return the already attached object.
  • If doesn't exist insert the new register to the database.

Here is a timeline of what you expect the behavior to be. I've abbreviated QuoteFragment as QF, assumed the existing Quote's quoteFragments set is empty at first (noted as {}), keep track of not yet persisted entities via color and assign an imaginary id to entities that are persisted.

expected situation

However, things are going differently in reality. The persist method takes the instance you provide and makes it managed. Changes to a managed entity are tracked and updates executed at the transaction commit (container-managed or explicit). So actually your merge(call) invocations aren't needed, this will be done anyway. The merge method, on the other hand, takes the instance you provide, finds the corresponding managed entity by primary key (fetched from storage if not yet cached in the EntityManager) and merges the state of your argument to that managed entity. If you call merge on an object that wasn't managed yet AND has no corresponding database entry, it will first make a copy of your provided object and persists that.

I'm not quite sure why this bit works for you

call = entityManager.persist( call );

because persist has a void return type. Maybe you use Hibernate-specific classes? It doesn't return anything to avoid this confusion. The object you pass as an argument will be the one to become managed.

However, when you call merge...

merge(someObject);

the return value from this merge is NOT necessarily the same instance as the argument you passed in. If you explicitly persisted it first, it will be, but as stated you won't need explicit merge calls anyway because changes are tracked. More specifically, if someObject doesn't correspond to any managed entity (either cached or in the datbase) by primary key it will in fact return a different instance, the one that is managed.

So what's actually happening is this:

actual situation

I'm not sure where the third entry comes from (maybe you explicitly persist the QuoteFragment somewhere else) but at this point it should be clear where the issues are. The best approach would be to persist the QuoteFragment first, explicitly, and then the objects depending on it. Calling merge on some entity you've just persisted or fetched from storage isn't necessary.

// later on at the persist stage..
// assuming all relations between the entities are still filled in (not set to null)

// persisting the new fragment
entityManager.persist(fragment);

// persisting the new call
entityManager.persist(call);

You'll probably have to change the cascade settings on field quote in Call and field fragments in Quote, otherwise you might have JPA throwing an error about trying to persist and already managed entity. I know this is probably exactly what you're trying to avoid (manually having to managed the persist order) but cascaded persists/merges are mostly useful for situations where you create an entire entity tree at once. In your case you have a previously created and retrieved entity (the Quote) and something that is referred in more than one place (the QuoteFragment). It's best not to rely on cascading in this case and do things explicitly.

Community
  • 1
  • 1
G_H
  • 11,739
  • 3
  • 38
  • 82
  • This may also prove useful: http://spitballer.blogspot.be/2010/04/jpa-persisting-vs-merging-entites.html – G_H Apr 25 '17 at 13:26
  • Wow thanks for the comprehensive answer! A note on the call = entityManager.persist(call) this was a bad copy/paste error on my part. I still dont understand why the first persist of newFrag is not the instance in the quote? At that point, the quote is the only entity holding onto it. Do you mean by setting call.setQuoteFragment(newFrag) earlier it is somehow still in there even though I set it back to null afterwards? – fancyplants Apr 25 '17 at 15:14
  • @fancyplants Setting it and then removing it again before persisting the `call` makes no difference. Up until that point the `call` is a simple POJO that the entity manager has no knowledge of. I believe that what happens it that as the "merge" operation is cascaded from the Call through the Quote to the QuoteFragment (in the Quote's `fragments` property), a copy of the QuoteFragment is persisted instead of the actual object you provide, because that's how "merge" deals with entities that aren't managed yet and have no primary key. – G_H Apr 25 '17 at 17:06
  • After some checking I've managed to fix things. I couldn't rely solely on the cascades as sometimes a call is persisted with a new quote and sometimes with an existing one, so some manual intervention was required. It turned out that sometimes an earlier update for related data cascaded a save to the existing quote, which saved the fragment. Then it saved again when I tried to update the call with the (unmanaged) fragment set. To fix things, I stopped the earlier update and reloaded the saved fragment from the database and set that to the call before saving. This fixed the problem. – fancyplants Apr 27 '17 at 14:38