-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 15 posts ] 
Author Message
 Post subject: Hibernate flush operation order and unique constraint
PostPosted: Thu Aug 03, 2017 9:53 am 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
Hi @all - I asked this already on stackoverflow, without success - probably here is a better place for my question.

Imagine the following scenario:

One table with topics, predecessor is linked to the same table / object:

Code:
|ID | PREDECESSOR | DESCRIPTION |
+---+-------------+-------------|
| 1 | NULL        | Topic 1     |
| 2 | 1           | Topic 2     |
| 3 | 2           | Topic 3     |


Column Predecessor has a unique constraint, because one topic can only be the predecessor of one topic.

Now we delete Topic 2 in our application. To achieve this we have to objects:

Code:
|ID | PREDECESSOR | DESCRIPTION |
+---+-------------+-------------|
| 2 | 1           | Topic 2       -> deleted
| 3 | 1           | Topic 3       -> new predecessor, because 2 was deleted


Now we save these objects within one transaction:

Code:
....
PersistentTransaction trans = session.beginTransaction();
session.saveOrUpdate(object topic 3);
session.delete(object topic 2); //object order does not matter
trans.commit();


Hibernate Log:

Code:
Hibernate:
update
    TOPIC
set
   .....


As you can see, unfortunately the update of topic 3 is called before deletion of topic 2 causing a

Code:
SqlException "Duplicate entry '1' for key 'UNIQUE_CONSTRAINT_NAME'


This exception is clear, but if the order would be "delete topic 2" and afterwards "update topic 3" everything would be fine.

Why is this constrained checked in the middle of the transaction? In my mind the whole transaction should be valid and the validations should run at the end of the transaction. Am I unaware of something?

It´s possible to recreate this priority problem with many other scenarios, so it is clear that there can´t be a strict order like "deletions first, then modifications, etc...". Or maybe it´s a matter of adjustment in the db? Can somebody give me a helping hand on this?

Thanks in advance,
Vincent

Hibernate 4.2.4; MariaDb 10.1.25; Java 1.8


Top
 Profile  
 
 Post subject: Re: Hibernate transaction validation / Object save priority
PostPosted: Thu Aug 03, 2017 10:14 am 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
That's because Hibernate uses a specific flush order which is explained by this article.

What you can do is to force a manual flush:

Code:
PersistentTransaction trans = session.beginTransaction();
session.delete(object topic 2); //object order does not matter

session.flush();

session.saveOrUpdate(object topic 3);
trans.commit();


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Thu Aug 03, 2017 10:58 am 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
Hello Vlad,

thank you very much for your fast response. In this case I´m catching the following exception:
Code:
deleted object would be re-saved by cascade (remove deleted object from associations):


Sure, I could remove the cascade, but I´m not sure if this is the correct approach for what I will achieve with my predecessors.

Here is my current annotation.

Code:
   @ManyToOne(targetEntity=Topic.class, fetch=FetchType.LAZY)   
   @org.hibernate.annotations.Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.LOCK})   
   @JoinColumns({ @JoinColumn(name="`PREDECESSOR`", referencedColumnName="`ID`") })   
   private Topic predecessor;


The second question would be:
In my application I collect all changed objects and save them within one transaction generically. So I don´t know the object order or the object relations during the save process.
I think to call a flush after every deletion would be very cost-intensive in relation to performance, wouldn´t it?


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Thu Aug 03, 2017 11:23 am 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
If the manual flush does not solve your issue, then you don't need it, right?

Your issue:

Code:
deleted object would be re-saved by cascade (remove deleted object from associations):


is caused because you set the cascade on the @ManyToOne associations which is almost always a code smell.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Thu Aug 03, 2017 12:13 pm 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
Okay, now it becomes even more strange...
Due to your statement of cascading @ManyToOne is almost never a good idea, I removed it. So the re-saved by cascade failure is gone of course.

If I don´t flush after deletion I still get the unique-constraint failure. So I inserted the flush again, as you expected:

Code:
PersistentTransaction trans = session.beginTransaction();
session.delete(object topic 2);
session.flush();
session.saveOrUpdate(object topic 3);
trans.commit();


Unfortunately hibernate is raising the same unique-constraint than before. Strangely enough I can´t see the delete-statement in the log. On session.flush() the output is:

Code:
update
        `TOPIC`
    set
        `CHDATE`=?,
        `CHUSER`=?,
        `PARENTID`=?,
        `PREDECESSOR`=?
    where                                 
        `ID`=?


Now my confusion is complete ;)


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Thu Aug 03, 2017 3:00 pm 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
Maybe you have a bidirectional association like the Parent also have a mappedBy reference back to its Child, and the delete is not issued because the Child is still associated to its Parent.

Otherwise, the delete should be executed. If it's not executed, try debugging the DefaultDeleteEventListener and see why it does not generate the DELETE statement.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 07, 2017 7:48 am 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
The problem seems to be triggered earlier in my process. There is no mappedBy, the delete-statement gets called correctly. Unfortunately the updates List in the ActionQueue is already filled.
This seems to happen "magically" when I do the

Code:
topic3.setPredecessor(topic1)


Why is this happening and can i prevent this? Is the Session listening to all its objects? I loaded the topics 1-3 before in the same session. Here again the simplified program sequence (translated generic code to spaghetti code for better comprehension :) ) :

Code:
Criteria crit = session.createCriteria(Topic.class.getName());
crit.add(......)
List<Topic> list = crit.list();
Topic topic1 = list.get(0):

...same for topic 2 and 3 ...

topic3.setPredecessor(topic1); //-> session is "magically" informed -> ActionQueue gets update-entry for topic3

session.delete(object topic 2); //-> no problem
session.flush(); //-> exception, because update is not possible because of the unique constraint and topic 2 isn´t deleted yet...

session.saveOrUpdate(object topic 3);


All cascades are off for the moment to prevent additional failures.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 4:26 am 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
I´ve created a test case for this issue. It seems like flushing the session would solve the problem. Unfortunately I´ve created a generic save method for all my objects, so I just can´t flush after every deletion. Is there any other possibility to solve this problem? Sth. like a Flush-OnDelete-annotation for attributes? ;)

I think I´m not the only one, saving his objects not in concrete methods. Another solution would be a database that supports deferred constraint checks :(

I hope someone has at least a straw to catch.

Code:
/*
* Copyright 2014 JBoss Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.bugs;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.Restrictions;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;

/**
* This template demonstrates how to develop a test case for Hibernate ORM,
* using its built-in unit test framework. Although ORMStandaloneTestCase is
* perfectly acceptable as a reproducer, usage of this class is much preferred.
* Since we nearly always include a regression test with bug fixes, providing
* your reproducer using this method simplifies the process.
*
* What's even better? Fork hibernate-orm itself, add your test case directly to
* a module's unit tests, then submit it as a PR!
*/
public class ORMUnitTestCase extends BaseCoreFunctionalTestCase {

  // Add your entities here.
  @Override
  protected Class[] getAnnotatedClasses() {
    return new Class[] { Topic.class };
  }

  // If you use *.hbm.xml mappings, instead of annotations, add the mappings
  // here.
  @Override
  protected String[] getMappings() {
    return new String[] {
        // "Foo.hbm.xml",
        // "Bar.hbm.xml"
    };
  }

  // If those mappings reside somewhere other than resources/org/hibernate/test,
  // change this.
  @Override
  protected String getBaseForMappings() {
    return "org/hibernate/test/";
  }

  // Add in any settings that are specific to your test. See
  // resources/hibernate.properties for the defaults.
  @Override
  protected void configure(Configuration configuration) {
    super.configure(configuration);

    configuration.setProperty(AvailableSettings.SHOW_SQL, Boolean.TRUE.toString());
    configuration.setProperty(AvailableSettings.FORMAT_SQL, Boolean.TRUE.toString());
    // configuration.setProperty( AvailableSettings.GENERATE_STATISTICS,
    // Boolean.TRUE.toString() );
  }

  /**
   * Create 3 Topics and save them in different orders. 1/2/3 - 2/3/1 - 1/3/2 -
   * 3/2/1
   *
   * @throws Exception
   */
  @Test
  public void createTopicsPerlmut() throws Exception {

    Session s = openSession();

    List<Topic> topicList = createTopics();

    saveTopics(s, topicList);

    topicList = createTopics();

    topicList.add(topicList.get(0));
    topicList.remove(0);

    saveTopics(s, topicList);

    topicList = createTopics();

    topicList.add(topicList.get(1));
    topicList.remove(1);

    saveTopics(s, topicList);

    // Reverse
    topicList = createTopics();

    Collections.reverse(topicList);

    saveTopics(s, topicList);

    s.close();

  }

  /**
   * Create 3 Topics. Save them. Load them again in new session. Delete last one
   * and therefore create a new one. save.
   *
   * @throws Exception
   */
  @Test
  public void createLoadAndChangeOrder() throws Exception {

    Session s = openSession();
    // Create and save 3 Topics
    List<Topic> topicList = createTopics();

    saveTopics(s, topicList);

    // Get topic 2
    Topic topic2 = topicList.get(1);

    // Get topic 3
    Topic topic3 = topicList.get(2);

    // Ensure to read the topics from database
    s.flush();
    s = openSession();

    Criteria crit = s.createCriteria(Topic.class.getName());

    crit.add(Restrictions.eq("id", topic3.getId()));

    @SuppressWarnings("unchecked")
    List<Topic> loadList = crit.list();

    Topic loadedTopic3 = loadList.get(0);

    crit = s.createCriteria(Topic.class.getName());

    crit.add(Restrictions.eq("id", topic2.getId()));

    loadList = crit.list();

    Topic loadedTopic2 = loadList.get(0);

    Transaction t = s.beginTransaction();

    // Delete Topic 3
    s.delete(loadedTopic3);

    // Is this flush replaceable with another construct?
    s.flush();
    // Create a new Topic for topic3, order after topic2 and save
    Topic topic4 = new Topic("Topic 4");
    topic4.setPredecessor(loadedTopic2);

    s.saveOrUpdate(topic4);

    t.commit();

  }

  /**
   * Create 3 Topics, delete the last one, create a new one, then save all.
   *
   * @throws Exception
   */
  @Test
  public void createAndChangeOrder() throws Exception {

    Session s = openSession();

    List<Topic> topicList = createTopics();

    saveTopics(s, topicList);

    // Get last topic
    Topic topic3 = topicList.get(2);

    Transaction transaction = s.beginTransaction();

    s.delete(topic3);
    s.flush();
    Topic topic4 = new Topic("Topic 4");
    Topic topic2 = topicList.get(1);
    topic4.setPredecessor(topic2);

    s.saveOrUpdate(topic4);

    transaction.commit();

  }

  /**
   * Save Topics.
   *
   * @param s
   * @param topicList
   * @throws PersistentException
   */
  private void saveTopics(Session s, List<Topic> topicList) throws Exception {

    Transaction transaction = s.beginTransaction();

    System.out.println("New transaction");

    for (Topic topic : topicList) {
      System.out.println(topic.getDescription());
      s.saveOrUpdate(topic);

    }

    transaction.commit();

  }

  /**
   * Create 3 Topics in a list with the order: 1,2,3.
   *
   * @return
   */
  private List<Topic> createTopics() {
    List<Topic> topicList = new ArrayList<>();

    Topic topic1 = new Topic("Topic 1");
    topicList.add(topic1);

    Topic topic2 = new Topic("Topic 2");
    topicList.add(topic2);

    Topic topic3 = new Topic("Topic 3");
    topicList.add(topic3);

    topic2.setPredecessor(topic1);
    topic3.setPredecessor(topic2);
    return topicList;
  }

}


Code:
package org.hibernate.bugs;
import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@org.hibernate.annotations.Proxy(lazy = false)
@Table(name = "TOPIC", schema = "PUBLIC")
public class Topic implements Serializable {
  public Topic() {
  }

  @Column(name = "ID", nullable = false, unique = true, length = 10)
  @Id
  @GeneratedValue(generator = "TOPIC_ID_GENERATOR")
  @org.hibernate.annotations.GenericGenerator(name = "TOPIC_ID_GENERATOR", strategy = "native")
  private int id;

  @Column(name = "DESCRIPTION", nullable = true, length = 255)
  private String description;

  @ManyToOne(targetEntity = Topic.class, fetch = FetchType.LAZY)
  @JoinColumns({ @JoinColumn(name = "PREDECESSOR", referencedColumnName = "ID", unique = true) })
  private Topic predecessor;

  @ManyToOne(targetEntity = Topic.class, fetch = FetchType.LAZY)
  @JoinColumns({ @JoinColumn(name = "PARENT", referencedColumnName = "ID") })
  private Topic parent;

  private void setId(int value) {
    this.id = value;
  }

  public int getId() {
    return id;
  }

  public int getORMID() {
    return getId();
  }

  public void setDescription(String value) {
    this.description = value;
  }

  public String getDescription() {
    return description;
  }

  public void setPredecessor(Topic value) {
    this.predecessor = value;
  }

  public Topic getPredecessor() {
    return predecessor;
  }

  public void setParent(Topic value) {
    this.parent = value;
  }

  public Topic getParent() {
    return parent;
  }

  public Topic(String description) {
    this.setDescription(description);
  }

  public String toString() {
    return String.valueOf(getId());
  }

}


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 7:46 am 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
Quote:
Is there any other possibility to solve this problem? Sth. like a Flush-OnDelete-annotation for attributes? ;)


Sure, there's such a possibility!

Instead of doing delete-flush-save which is both inefficient and generates your issue, you can simply update the entity that has the same natural key constraint as the one that you want to overwrite it with the latest entity snapshot.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 11:34 am 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
Just to be sure I understood you right this would be the best solution?:

Code:
    topic3.setPredecessor(null); //(was topic 2 until now)
    s.saveOrUpdate(topic3);
    s.delete(topic3);
    Topic topic4 = new Topic("Topic 4");
    Topic topic2 = topicList.get(1);
    topic4.setPredecessor(topic2);
    s.saveOrUpdate(topic4);


So before I delete a object i have to detach all referenced objects save and afterwards delete it?
In any case this solution works and im confident I can solve all my dependency-problems with this approach.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 11:50 am 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
Quote:
Just to be sure I understood you right this would be the best solution?:


Nope, it's not the best solution. The best solution is not to issue the delete at all. Just fetch the entity but it's @NaturalId and update it accordingly.


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 12:26 pm 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
But I want to delete the second topic. If I just detach it´s predecessor I have an unused orphan topic.

This was the example: create 3 topics, delete the last one (maybe this one was wrong) and create a new one.
Sure I could use the old topic3 an change all it´s values to the new ones, but unfortunately I don´t know what the user wants to do with his topics, before he saves them. Maybe he deletes 7 topics and creates 5 new topics, afterwards he changes their order(predecessors) an then hits the save-button.
So I have to collect all of them and try to persist them :(


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 12:36 pm 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
But you can also do a match between the previous child entities and the incoming ones. If you have a natural id, otherwise there would be no unique constraint conflicts, the match can be done by the natural id so that:

- the new entries are persisted
- the entries that can no longer be found are deleted
- the remaining ones are updated


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 12:53 pm 
Newbie

Joined: Thu Aug 03, 2017 4:02 am
Posts: 12
Okay, this sounds like the cleanest solution, but needs much more efford in the backend. I have to think about this as a longterm solution, for the short run I can solve it with the unclean "detach everything" solution.
Thank you very much for your help!


Top
 Profile  
 
 Post subject: Re: Hibernate flush operation order and unique constraint
PostPosted: Mon Aug 28, 2017 2:55 pm 
Hibernate Team
Hibernate Team

Joined: Thu Sep 11, 2014 2:50 am
Posts: 1628
Location: Romania
You're welcome. Although slightly more work to do, the solution I gave you is both effective and more efficient (think about index entries you delete only to recreate them back). On the long-term, it's better to do the right thing.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 15 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.