0

I have a class that has the following field:

@Version
private Long version = 1L;

public Long getVersion() {
    return version;
}

public void setVersion(Long version) {
    this.version = version;
}

I use Spring web + Hibernate. I hope to see a relatively complete example of using @Version to prevent simultaneous edits to the same record.

Googled and searched SO. Failed to find any. I used a hidden HTML form field for the version field and it is not working.

Any info/pointer is really appreciated.

Thanks and regards!

Update 1

Here is what I did. I loaded the same record in two browser windows. I was able to see that there was a hidden version field with the same value in the HTML form of both windows. I submitted the form in one window and checked the database and notice that the Version field was increased. Then I submitted another window. Nothing happened and the record was updated/saved successfully. I was expecting some sort of exception was thrown.

Do I need to configure the spring/hibernate combination to make it work, in addition to the Version field definition?

Update 2

I did the following to trace how Hibernate update the record:

hibernate.show_sql=true

log4j.logger.org.hibernate.type=trace

Now I am able to see how Hibernate update the record. I notice that at the moment of Hibernate updating the record with the following SQL

update ... where id=? and version=?

Hibernate always uses the latest version number from the database to bind, even though I can confirm that in the web layer a record still has the old version number plus other fields with new data before saving the record.

How come?

I feel optimistic locking is disabled somehow in my app, but I do not do Hibernate config for this. I am using Hibernate 4.2.1.Final and Spring 3.2.2.RELEASE.

Is there a way to explicit to enable/disable Hibernate optimistic locking?

Update 3

The following is the data objects and database layer code. "friendGroupDao" is auto-wired into the transactional service layer where this DAO object is called to load an object to the web layer and saves an object to the database. The related service layer methods simply call the friendGroupDao.load() and friendGroupDao.save() without any additional code.

-------------- domain objects -------------

@MappedSuperclass
public abstract class BaseObject implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue( strategy = GenerationType.IDENTITY )
    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Version
    private Long version = 1L;

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
            this.version = version;
    }   

    public BaseObject() {
    }

}

@Entity
public class FriendGroup extends BaseObject {

    @Column(columnDefinition = "NVARCHAR(120)")
    private String name;

    public String getName() {
        return name;
    }

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

--------------- database -------------

public interface BaseObjectDao<T extends BaseObject> {

    void delete(T o);

    T load(Long id);

    void save(T o);
}

public class BaseObjectDaoImpl<T extends BaseObject> implements
        BaseObjectDao<T> {

    private Class<T> domainClass;

    @Autowired
    protected SessionFactory sessionFactory;

    public BaseObjectDaoImpl() {
        this.domainClass = (Class<T>) BaseObject.class;
    }

    public BaseObjectDaoImpl(Class<T> domainClass) {
        this.domainClass = domainClass;
    }

    public SessionFactory getSessFactory() {
        return sessionFactory;
    }

    public void setSessFactory(SessionFactory sf) {
        this.sessionFactory = sf;
    }

    public void delete(T object) {
        getSession().delete(object);
    }

    public T load(Long id) {
        return (T) getSession().get(domainClass, id);
    }

    public void save(T object) {    
        getSession().saveOrUpdate(object);
    }

    public Session getSession() {
        return sessionFactory.getCurrentSession();
    }
}

public interface FriendGroupDao extends BaseObjectDao<FriendGroup> {
}

@Repository("friendGroupDao")
public class FriendGroupDaoImpl extends BaseObjectDaoImpl<FriendGroup> implements
    FriendGroupDao {

    public FriendGroupDaoImpl() {
        super(FriendGroup.class);
    }
}
4

1 回答 1

3

Here is an Example how you'll get an OptimisticLockException (the code can be written in a better / more elegant way but for demonstration purposes I think it's ok)

    package com.example.jpa;


    import java.util.concurrent.Callable;

    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.Persistence;

    public class JPASample {

        private EntityManager entityManager;

        private Semaphore semaphore;

        void run(String [] args) throws Exception {
            EntityManagerFactory factory = Persistence.createEntityManagerFactory("JPASample");
            entityManager = factory.createEntityManager();

            try {

                FirstEntity entity = createEntity();

                ReadValueCmd readValueCmd = new ReadValueCmd(entity.getId());
                entityManager.getTransaction().begin();
                FirstEntity e1 = readValueCmd.call();
                entityManager.getTransaction().commit();
                entityManager.detach(e1);

                entityManager.getTransaction().begin();
                FirstEntity e2 = readValueCmd.call();
                entityManager.getTransaction().commit();
                entityManager.detach(e2);

                e1.setValue(e1.getValue()+1);
                e2.setValue(e1.getValue()+1);

                UpdateEntityCmd updateCmd1 = new UpdateEntityCmd(e1);
                entityManager.getTransaction().begin();
                updateCmd1.call();
                entityManager.getTransaction().commit();

                UpdateEntityCmd updateCmd2 = new UpdateEntityCmd(e2);
                entityManager.getTransaction().begin();
                updateCmd2.call();
                entityManager.getTransaction().commit();



            } finally {
                entityManager.close();  
            }       
        }

        public static void main(String[] args) throws Exception{
            new JPASample().run(args);
        }

        private class ReadValueCmd implements Callable<FirstEntity> {

            private long id;

            private ReadValueCmd(long id) {
                this.id = id;
            }

            @Override
            public FirstEntity call() throws Exception {
                FirstEntity entity; 
                entity = entityManager.find(FirstEntity.class, id);
                System.out.println("entity read: " + entity);

                return entity;
            }

        }

        private class UpdateEntityCmd implements Callable<FirstEntity> {

            private FirstEntity entity;

            private UpdateEntityCmd(FirstEntity entity) {
                this.entity = entity;
            }

            @Override
            public FirstEntity call() throws Exception {
                entity = (FirstEntity)entityManager.merge(entity);
                System.out.println("entity merged: " + entity);

                return entity;
            }
        }   



    private FirstEntity createEntity () {
        FirstEntity entity = new FirstEntity();
        entity.setName("initialName");
        entityManager.getTransaction().begin();
        entityManager.persist(entity);
        entityManager.getTransaction().commit();

        return entity;
    }
   }


package com.example.jpa;

import java.io.Serializable;
import java.math.BigInteger;

import javax.persistence.*;

@Entity
public class FirstEntity implements Serializable {


    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long firstEntityId;

    @Version
    private long versionField;

    private long value;

    public long getId() {
        return firstEntityId;
    }

    @Override
    public String toString() {
        return "FirstEntity [firstEntityId=" + firstEntityId
                + ", versionField=" + versionField + ", value=" + value
                + ", name=" + name + "]";
    }

    public long getValue() {
        return value;
    }

    public void setValue(long value) {
        this.value = value;
    }

    private String name;

    public FirstEntity() {
        super();
    }

    public void setName(String newName) {
        name = newName;
    }

    public String getName() {
        return name;
    }

    public long getVersionField() {
        return versionField;
    }

    public void setVersionField(long versionField) {
        this.versionField = versionField;
    }  
}

The outcome is something like

Exception in thread "main" javax.persistence.OptimisticLockException at org.hibernate.ejb.AbstractEntityManagerImpl.wrapStaleStateException(AbstractEntityManagerImpl.java:1403) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1319) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1300) at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1306) at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:888) at com.example.jpa.JPASample$UpdateEntityCmd.call(JPASample.java:124) at com.example.jpa.JPASample.run(JPASample.java:47) at com.example.jpa.JPASample.main(JPASample.java:61) Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.jpa.FirstEntity#1] at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303) at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151) at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76) at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:903) at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887) at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891) at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:879) ... 3 more

于 2013-06-22T09:44:00.963 回答