Announcement Announcement Module
Collapse
No announcement yet.
How to do optimistic locking in Spring + Hibernate? Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • How to do optimistic locking in Spring + Hibernate?

    Hi, I am new to Spring and Hibernate. I find the very challenging thing in using Spring is configuration and settings. Currently I am doing a
    Spring + Hibernate web application.

    I need to implement optimistic locking to achieve data consistency
    for concurrent users. I have a domain class User. Here is the User.hbm.xml:

    <class name="User" table="USER" optimistic-lock="version">
    <id name="id" type="long" column="ID">
    <meta attribute="scope-set">private</meta>
    <generator class="native"/>
    </id>
    <timestamp name="lastUpdatedDateTime" column="LAST_UPDATED_DATETIME">
    <meta attribute="scope-set">private</meta>
    </timestamp>
    ......


    My web controller class calls a UserManager service class which calls
    a DAO which actually saves the data.

    My web controller class tries to catch OptimisticLockingFailureException, but I did numerous tests and there was never an exception of that class happened. A updated user is always saved without any error. I noticed
    that Hibernate correctly updates timestamp.

    Did I miss something or do something wrong in Spring settings or configurations? How do I do optimistic locking in Spring + Hibernate web
    application?

    Thanks very much for your attention and advice!

    Pete

  • #2
    A timestamp should work, however a version is better. This shouldn't be the problem though.

    Remember, Spring does not do anything special with respect to Hibernate. Have you had a look at the Hibernate doco to see what they say about it?

    regards,
    Alef Arendsen

    Comment


    • #3
      You should check out Hibernate's documentation for the <version> tag. Basically, Hibernate supports a version number or timestamp for its versioning and will manage the <version> values for you.

      There really isn't anything to do in Spring to enable this as it's a Hibernate thing.

      Comment


      • #4
        Problem with optimistic locking

        Hi, I am new to Spring and Hibernate.
        I try to use optimistic locking within a Spring MVC Webapplication.
        (I think my problem related to Pete's problem descibed above)

        To realize optimistic locking I added a version column to the customer database table and added a <version>-tag to the hibernate mapping.

        When I try to simulate a lost-update situation the lost update occurs and no exception is thrown

        The version-value is incremented correctly whenever an update occurs.

        The problem is that when I load the same customer in (e.g.) two different browser windows, both FormControllers hold a reference to the same (physical) object.
        when the first browser persists the changes, the version number of the customerObject being referenced by the second Controller "is incremented too" (same object). Therefore there is no reason to throw an exception when the second browser tries to persist the changes.

        above I attached the sourcecode of the (in my opinion) relevant files.


        I hope someone can help me.

        Many thanks in advance.

        Regards

        Thomas

        hibernate mapping:
        Code:
        <class name="Customer" optimistic-lock="version" dynamic-insert="true" dynamic-update="true" >
          	<id name="id" type="long" column="CUSTID">
          		<generator class="increment"/>
          	</id>
          	 
          	<version name="version" access="field" column="version" type="integer"/>
          	
          	<property name="firstname" length="50" type="string"/>
          	<property name="lastname" length="50" type="string"/>
          	
          </class>
        the Customer Class:

        Code:
        package de.uni.erlangen.swtmfisc.exampleapp.domain;
        
        import de.uni.erlangen.swtmfisc.domain.DomainObject;
        
        public class Customer extends DomainObject<Long> {
        	private String firstname;
        	private String lastname;
        	
        	public String getFirstname() {
        		return firstname;
        	}
        	public void setFirstname(String firstname) {
        		this.firstname = firstname;
        	}
        	public String getLastname() {
        		return lastname;
        	}
        	public void setLastname(String lastname) {
        		this.lastname = lastname;
        	}
        	
        	@Override
        	public String toString() {
        		return firstname + " " + lastname + " [" + super.toString() + "]";
        	}
        }
        the DomainObject Class:

        Code:
        package de.uni.erlangen.swtmfisc.domain;
        
        import java.io.Serializable;
        import java.util.Date;
        
        public abstract class DomainObject<I extends Serializable> {
        	private I id;
        	private Integer version;	/* used for optimistic locking */
        	
        	public Integer getVersion() {
        		return version;
        	}
        
        	@Override
        	public boolean equals(Object obj) {
        		if (this == obj) {
        			return true;
        		}
        		if (!(obj instanceof DomainObject)) {
        			return false;
        		}
        		if (this.id == null || ((DomainObject) obj).getId() == null) {
        			return false;
        		}
        		return id.equals(((DomainObject) obj).id);
        	}
        	
        	@Override
        	public int hashCode() {
        		if (id != null) {
        			return id.hashCode();
        		}
        		return super.hashCode();
        	}
        
        	public I getId() {
        		return id;
        	}
        
        	public void setId(I id) {
        		this.id = id;
        	}
        }
        the AbstractHibernateDao (this is the Super-Class of the CustomerHibernateDao being used by the CustomerService to retrieve and store Customer-Objects)

        Code:
        @Transactional	/* All public methods within the class are executed in a separate transaction */
        public abstract class AbstractHibernateDao<T extends DomainObject<I>, I extends Serializable> 
        		extends HibernateDaoSupport implements Dao<T, I> {
        	private final Class<T> objectClass;
        	private final Class<I> keyClass;
        	
        	protected AbstractHibernateDao(final Class<T> objectClass, final Class<I> keyClass) {
        		this.keyClass = keyClass;
        		this.objectClass = objectClass;
        	}
        	
        	protected Class<T> getObjectClass() {
        		return objectClass;
        	}
        
        	protected Class<I> getKeyClass() {
        		return keyClass;
        	}
        	
        	public void insert(final T entity) {
        		if (entity == null) {
        			return;
        		}
        		getHibernateTemplate().saveOrUpdate(entity);
        	}
        	
        	@SuppressWarnings("unchecked")
        	@Transactional(readOnly = true)
        	public final List<T> findAll() {
        		return getHibernateTemplate().loadAll(getObjectClass());
        	}
        
        	@SuppressWarnings("unchecked")
        	public final T findById(I id) {
        		return (T) getHibernateTemplate().get(objectClass, id);
        	}
        	
        	public final void delete(T entity) {
        		getHibernateTemplate().delete(entity);
        	}
        	
        	public final void update(T entity) {
        		if (entity == null) {
        			return;
        		}
        		getHibernateTemplate().update(entity);
        	}
        }

        Comment


        • #5
          Walk me through exactly how your controller is backing, binding and persisting. Preferably post the related source code.

          Comment


          • #6
            Hello jglynn

            thanks for your reply.

            The relevant Controller:

            Code:
            package de.uni.erlangen.swtmfisc.exampleapp.web;
            
            import javax.servlet.http.HttpServletRequest;
            import javax.servlet.http.HttpServletResponse;
            
            /* [...]*/
            
            
            import de.uni.erlangen.swtmfisc.exampleapp.domain.Customer;
            import de.uni.erlangen.swtmfisc.exampleapp.service.CustomerService;
            
            public class EditCustomerController extends SimpleFormController {
            	private CustomerService customerService;
            	private Log logger = LogFactory.getLog(getClass());
            	
            	public CustomerService getCustomerService() {
            		return customerService;
            	}
            
            	public void setCustomerService(CustomerService customerService) {
            		this.customerService = customerService;
            	}
            
            	public EditCustomerController() {
            		setCommandClass(Customer.class);
            		setCommandName("customer");
            		setSessionForm(true);
            	}
            	
            	@Override
            	protected Object formBackingObject(HttpServletRequest request)
            			throws Exception {
            		String custID = request.getParameter("custID");
            		Customer customer = customerService.findCustomerById(Long.parseLong(custID))<D-c>;
            		if (customer != null) {
            			logger.info("version: " + customer.getVersion());
            		}
            		return customer; 
            	}
            	
            	@Override
            	protected ModelAndView onSubmit(HttpServletRequest request,
            			HttpServletResponse response, Object command, BindException errors)
            			throws Exception {
            		Customer customer = (Customer) command;
            		logger.info("before update with version: " + customer.getVersion() + customer);
            		customerService.updateCustomer(customer);
            		logger.info("after update with version: " + customer.getVersion() + customer);
            		return new ModelAndView(getSuccessView());
            	}
            }
            This controller calls a CustomerService to retreive and store a Customer Object
            Customer customer = customerService.findCustomerById(Long.parseLong(cu stID));
            and:
            customerService.updateCustomer(customer);



            The CustomerService-Interface is implemented by CustomerServiceImpl:

            Code:
            package de.uni.erlangen.swtmfisc.exampleapp.service.impl;
            
            import java.util.List;
            
            import de.uni.erlangen.swtmfisc.exampleapp.dao.CustomerDao;
            import de.uni.erlangen.swtmfisc.exampleapp.domain.Customer;
            import de.uni.erlangen.swtmfisc.exampleapp.service.CustomerService;
            
            public class CustomerServiceImpl implements CustomerService {
            	private CustomerDao customerDao;
            	
            	public CustomerDao getCustomerDao() {
            		return customerDao;
            	}
            
            	public void setCustomerDao(CustomerDao customerDao) {
            		this.customerDao = customerDao;
            	}
            
            	public List<Customer> getCustomers() {
            		return customerDao.findAll();
            	}
            
            
            	public Customer findCustomerById(Long custID) {
            		return customerDao.findById(custID);
            	}
            
            	public void createNewCustomer(Customer cust) {
            		customerDao.insert(cust);
            		
            	}
            
            	public void updateCustomer(Customer cust) {	
            		customerDao.update(cust);
            	}
            
            	public void deleteCustomer(Customer cust) {
            		customerDao.delete(cust);
            	}
            }
            This service uses a simple CustomerDAO to retrieve and persist Objects:


            Code:
            package de.uni.erlangen.swtmfisc.exampleapp.dao.hibernate;
            
            import java.util.List;
            
            import org.hibernate.criterion.DetachedCriteria;
            import org.hibernate.criterion.Order;
            import org.hibernate.criterion.Restrictions;
            
            import de.uni.erlangen.swtmfisc.dao.hibernate.AbstractHibernateDao;
            import de.uni.erlangen.swtmfisc.exampleapp.dao.CustomerDao;
            import de.uni.erlangen.swtmfisc.exampleapp.domain.Customer;
            
            public class CustomerHibernateDaoImpl extends AbstractHibernateDao<Customer, Long>
            		implements CustomerDao {
            
            	public CustomerHibernateDaoImpl() {
            		super(Customer.class, Long.class);
            	}
            	
            	@SuppressWarnings("unchecked")
            	public List<Customer> findByFirstName(String firstname) {
            		DetachedCriteria criteria = DetachedCriteria.forClass(getObjectClass());
            	    criteria.add(Restrictions.ilike("firstname", firstname != null ? firstname + "%" : ""));
            	    criteria.addOrder(Order.asc("firstname"));
            	    return getHibernateTemplate().findByCriteria(criteria);
            	}
            
            	@SuppressWarnings("unchecked")
            	public List<Customer> findByFullName(String firstname, String lastname) {
            		DetachedCriteria criteria = DetachedCriteria.forClass(getObjectClass());
            	    criteria.add(Restrictions.ilike("firstname", firstname != null ? lastname + "%" : ""));
            	    criteria.add(Restrictions.ilike("lastname", lastname != null ? lastname + "%" : ""));
            	    criteria.addOrder(Order.asc("lastname"));
            	    criteria.addOrder(Order.asc("firstname"));
            	    return getHibernateTemplate().findByCriteria(criteria);
            	}
            
            	@SuppressWarnings("unchecked")
            	public List<Customer> findByLastName(String lastname) {
            		DetachedCriteria criteria = DetachedCriteria.forClass(getObjectClass());
            	    criteria.add(Restrictions.ilike("lastname", lastname != null ? lastname + "%" : ""));
            	    criteria.addOrder(Order.asc("lastname"));
            	    return getHibernateTemplate().findByCriteria(criteria);
            	}
            }
            The relevant Methods (customerDao.findById(custID); customerDao.update(cust);) are implemented
            in the super-calss of the CustomerHibernateDAO:

            Code:
            package de.uni.erlangen.swtmfisc.dao.hibernate;
            
            import java.io.Serializable;
            import java.util.List;
            
            import org.hibernate.StaleObjectStateException;
            import org.springframework.dao.DataAccessException;
            import org.springframework.dao.OptimisticLockingFailureException;
            import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
            import org.springframework.transaction.annotation.Transactional;
            
            import de.uni.erlangen.swtmfisc.dao.Dao;
            import de.uni.erlangen.swtmfisc.domain.DomainObject;
            
            @Transactional	/* All public methods within the class are executed in a separate transaction */
            public abstract class AbstractHibernateDao<T extends DomainObject<I>, I extends Serializable> 
            		extends HibernateDaoSupport implements Dao<T, I> {
            	private final Class<T> objectClass;
            	private final Class<I> keyClass;
            	
            	protected AbstractHibernateDao(final Class<T> objectClass, final Class<I> keyClass) {
            		this.keyClass = keyClass;
            		this.objectClass = objectClass;
            	}
            	
            	protected Class<T> getObjectClass() {
            		return objectClass;
            	}
            
            	protected Class<I> getKeyClass() {
            		return keyClass;
            	}
            	
            	public void insert(final T entity) {
            		if (entity == null) {
            			return;
            		}
            		getHibernateTemplate().saveOrUpdate(entity);
            	}
            	
            	@SuppressWarnings("unchecked")
            	@Transactional(readOnly = true)
            	public final List<T> findAll() {
            		return getHibernateTemplate().loadAll(getObjectClass());
            	}
            
            	@SuppressWarnings("unchecked")
            	public final T findById(I id) {
            		return (T) getHibernateTemplate().get(objectClass, id);
            	}
            	
            	public final void delete(T entity) {
            		getHibernateTemplate().delete(entity);
            	}
            	
            	public final void update(T entity) {
            		if (entity == null) {
            			return;
            		}
            		getHibernateTemplate().update(entity);
            	}
            }


            These are the my config-files for the application-context:

            Hibernate-Configuration:
            Code:
            <beans xmlns="http://www.springframework.org/schema/beans"
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                     xmlns:aop="http://www.springframework.org/schema/aop"
                     xmlns:tx="http://www.springframework.org/schema/tx"
                     xsi:schemaLocation="http://www.springframework.org/schema/beans 
                       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                       http://www.springframework.org/schema/aop 
                       http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
                       http://www.springframework.org/schema/tx 
                       http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
                    
            	<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
               		<property name="dataSource" ref="dataSource"/>
               		<property name="mappingResources">
               			<list>
               				<value>/hibernate/customer.hbm.xml</value>
               			</list>
               		</property>   
               		<property name="hibernateProperties">
               			<props>
               				<prop key="hibernate.dialect">
               					${database.dialect}
               				</prop>
               				<prop key="hibernate.show_sql">true</prop>
               				<prop key="hibernate.jdbc.batch_size">100</prop>
               				<prop key="hibernate.hbm2dll.auto">update</prop>
               			</props>
               		</property>
               </bean>  
                       
            </beans>
            Persitence Configuration:

            Code:
            <beans xmlns="http://www.springframework.org/schema/beans"
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                     xmlns:aop="http://www.springframework.org/schema/aop"
                     xmlns:tx="http://www.springframework.org/schema/tx"
                     xsi:schemaLocation="http://www.springframework.org/schema/beans 
                       http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                       http://www.springframework.org/schema/aop 
                       http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
                       http://www.springframework.org/schema/tx 
                       http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
             	
             	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
                  	<property name="driverClassName" value="${database.driver}"/>
            	    <property name="url" value="${database.server}"/>
            	    <property name="username"  value="${database.user}"/>
            	    <property name="password" value="${database.password}"/>
                </bean>
                
            	
                <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
                	<property name="locations">
                        <list>
                            <value>classpath:/database/localHSQLdatabase.properties</value>
                        </list>
                	</property>
                </bean>      
                
               <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
               		<property name="sessionFactory" ref="sessionFactory"/>
               </bean>
            </beans>

            Comment


            • #7
              Hello jglynn,

              it was my mistake. I used two different browser-tabs to simulate a dirty-update. therefore the two tabs shared one http-session and no optimistic locking exception occured.
              using two different browsers works fine

              Comment


              • #8
                Did you also test the following:

                Perform an update through your application, but set a breakpoint so the update isn't immediately executed.
                Then, directly modify the corresponding record in the database using eq Oracle Sql Developer, incrementing the version number/changing the timestamp and commiting the change, then go back to your webapp, and resume execution.

                Did you then also get an exception ?

                Comment


                • #9
                  Did you also test the following:

                  Perform an update through your application, but set a breakpoint so the update isn't immediately executed.
                  Then, directly modify the corresponding record in the database using eq Oracle Sql Developer, incrementing the version number/changing the timestamp and commiting the change, then go back to your webapp, and resume execution.

                  Did you then also get an exception ?

                  Comment

                  Working...
                  X