1

Previously, when I was adding a entity to database with Hibernate I used to check that it hadn't already been added. But in an effort to improve performance I forgot this check and just tried to add without checking, as I was using saveOrUpdate() it was my understanding that if Hibernate found it was already added it would just update with and changes made by my save.

But instead it fails with

18/08/2018 21.58.34:BST:Errors:addError:SEVERE: Adding Error:Database Error:Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.jthink.songlayer.MusicBrainzReleaseWrapper#95f6f584-407f-4b26-9572-bb8c6e9c580a]
java.lang.Exception
    at com.jthink.songkong.analyse.general.Errors.addError(Errors.java:28)
    at com.jthink.songkong.exception.ExceptionHandling.handleHibernateException(ExceptionHandling.java:209)
    at com.jthink.songkong.db.ReleaseCache.addToDatabase(ReleaseCache.java:394)
    at com.jthink.songkong.db.ReleaseCache.add(ReleaseCache.java:65)




@Entity
    public class MusicBrainzReleaseWrapper
    {
        @Id
        private String guid;

        @Version
        private int version;

        @org.hibernate.annotations.Index(name = "IDX__MUSICBRAINZ_RELEASE_WRAPPER_NAME")
        @Column(length = 1000)
        private String name;

        @Lob
        @Column(length = 512000)
        private String xmldata;

        public String getGuid()
        {
            return guid;
        }

        public void setGuid(String guid)
        {
            this.guid = guid;
        }

        public String getName()
        {
            return name;
        }

        public void setName(String name)
        {
            this.name = name;
        }

        public String getXmldata()
        {
            return xmldata;
        }

        public void setXmldata(String xmldata)
        {
            this.xmldata = xmldata;
        }
    }

    private static boolean addToDatabase(Release release)
        {
            Session session = null;
            try
            {
                session = HibernateUtil.beginTransaction();
                //Marshall to String
                StringWriter sw = new StringWriter();
                Marshaller m = jc.createMarshaller();
                m.marshal(release, sw);
                sw.flush();

                MusicBrainzReleaseWrapper wrapper = new MusicBrainzReleaseWrapper();
                wrapper.setGuid(release.getId());
                wrapper.setName(release.getTitle().toLowerCase(Locale.UK));
                wrapper.setXmldata(sw.toString());
                session.saveOrUpdate(wrapper);
                session.getTransaction().commit();
                MainWindow.logger.info("Added to db release:" + release.getId() + ":" + release.getTitle());
                return true;
            }
            catch (ConstraintViolationException ce)
            {
                MainWindow.logger.warning("Release already exists in db:"+release.getId()+":"+release.getTitle());
                return true;
            }
            catch(GenericJDBCException jde)
            {
                MainWindow.logger.log(Level.SEVERE, "Failed:" +jde.getMessage());
                ExceptionHandling.handleDatabaseException(jde);
            }
            catch(HibernateException he)
            {
                MainWindow.logger.log(Level.SEVERE, "Failed:" +he.getMessage());
                ExceptionHandling.handleHibernateException(he);
            }
            catch(Exception e)
            {
                MainWindow.logger.log(Level.WARNING,"Failed AddReleaseToDatabase:"+release.getId()+ ':' +e.getMessage(),e);
                throw new RuntimeException(e);
            }
            finally
            {
                HibernateUtil.closeSession(session);
            }
            return false;
        }

Used to check first before call to addToDatabase

        if(ReleaseCache.get(release.getId())==null)
        {
            addToDatabase(release)
        }        
P3trur0
  • 3,155
  • 1
  • 13
  • 27
Paul Taylor
  • 13,411
  • 42
  • 184
  • 351

4 Answers4

2

Hiberante object has 3 states for an Entity. They are: - Transient Or New - Detached (Objects are fetched from DB and hibernate session is closed) - Persistent (Object are fetched from DB and hibernate session is open) In saveOrUpdate method, it either save the transient object or update the detached/ persistent object. In your code, you are trying to create Transient/New object and setting the old id in it. That's the reason you are getting above error. The correct way to fetch the object first using id and then update it.

Pavan
  • 121
  • 4
1

No. saveOrUpdate method is used either to persist or merge an entity with the current session. It doesn't do what you expect. Either save or update entity is application's specific logic. Hibernate doesn't do any application's specific logic.

menteith
  • 596
  • 14
  • 51
Ilya Dyoshin
  • 4,459
  • 1
  • 19
  • 18
1

Session.merge() can directly save a previously unknown instance, but note it won't necessarily avoid the extra select against the database.

@Pavan is right about the entity being transient or detached in Hibernate (or JPA) terminology. Both of these states mean that Hibernate has not yet got a reference to this instance of the entity in its session (in the StatefulPersistenceContext), but detached clearly means it is known to the database.

  • merge() instructs Hibernate to stop and check for a detached instance. The first check is for the @Id value in the session, but if it's not already there, it must hit the database.
  • saveOrUpdate() instructs Hibernate that the caller knows it is safe to only check the StatefulPersistenceContext for the @Id. If it's not there, the entity is assumed to be transient (i.e. new), and Hibernate will proceed to the insert operation.

saveOrUpdate() is good for instances (with or without an @Id value) that are known to the session already.

In your case clearly Hibernate is unaware of the detached instance, so you would need to use merge(). But that also means Hibernate has to check the database for the instance it hasn't seen before - if the entity has an @Id value.

To come back to the original intent in your question, update without select is harder ...

For an update, Hibernate likes to know the prior state of the entity. This makes sense if it's using dynamic updates (so not updating all columns), but otherwise you would think it could go straight for the update. The only option I know of for this is a direct update query (via HQL or JPQL), but this is hardly convenient if you have an entity instance. Maybe someone else knows how to do this.

df778899
  • 10,703
  • 1
  • 24
  • 36
1

The problem you are hitting is directly related to the Optimistic locking you have enabled through the @Version annotation on the MusicBrainzReleaseWrapper. saveOrUpdate really can either add or update an entity but this is only if the entity version is the same as the one of the detached object you are trying to add or merge.

In your particular example your detached object has a version previous to the last version in the database therefore the operation can not be executed on a stale data.

UPDATE:

MusicBrainzReleaseWrapper wrapper = session.get(release.getId()):
//the wrapper is managed object
if (wrapper == null) {
  //initilize wrapper with the values from release
  .......
  session.save(wrapper)
}
else {
   // do not set ID here. ID is aready present!!!
   //  never manuay set the version field here
   wrapper.setName(release.getTitle().toLowerCase(Locale.UK));
   wrapper.setXmldata(sw.toString());
   session.saveOrUpdate(wrapper);
   //In case you don't need update logic at all
   // remove the @Version field from the entity
   // and do othing in the else clause , or throw exception
   // or log error or anything you see fit
}
Alexander Petrov
  • 9,204
  • 31
  • 70
  • Okay I think I understand, so how should I deal with this. Its worth noting if already in database then I dont need to do anything, i.e im trying to do INSERT IF RECORD WITH THIS GUID DOESNT ALREADY EXIST, if it already exists then there is no requirement to update it, but Im not very keen on logic that throws exception. If I was using SQL I would do INSERT SELECT LEFT JOIN ON ITSELF WHERE ID=NULL to ensure I only did an insert when record didnt already exist, but what is the Hibernate erquivalent ? – Paul Taylor Sep 11 '18 at 16:21
  • So if I remove @Version that means I dont do optimistic locking, and therefore does that mean table is locked whenever I try to retrieve from it or only when do a save, or something else ? – Paul Taylor Sep 12 '18 at 08:15
  • No , if you remove the @version this would mean that the last to commit wins. Withoptimistic locking on it is first to commit wins. – Alexander Petrov Sep 12 '18 at 09:43
  • Check out this post for more detaild explanation on optimistisk locking vs other types https://stackoverflow.com/questions/38303012/how-to-properly-handle-two-threads-updating-the-same-row-in-a-database/38444458#38444458 – Alexander Petrov Sep 12 '18 at 09:44
  • I thought that turned it to pessimistic but I see not now, but okay last commit wins would work. But is there not a way to do INSERT IF DOESNT EXIST, the call to session.get() is not the same because 1> it requites two calls (get/save), even if get is null it could have been created by another thread when actually do save, although I suppose this doesnt really matter as it wont fail anymore. – Paul Taylor Sep 12 '18 at 09:49
  • There is no way. JPA/Hibernate is generic framework. It can not replace fully SQL. – Alexander Petrov Sep 12 '18 at 10:14
  • But could I use HQL ? – Paul Taylor Sep 12 '18 at 11:02
  • No, hql is not sql either. – Alexander Petrov Sep 12 '18 at 12:28
  • okay could I use pure sql (if I removed the @version field as I dont know what to set that to) without breaking HIbernate ? – Paul Taylor Sep 12 '18 at 13:50
  • I have marked correct as I now understand the issue, if the object exists and i want to do the update part of saveAndUpdate then I need to retrieve from HIbernateSession, but instead I am just creating a new object. – Paul Taylor Sep 12 '18 at 13:52