Reputation: 131
I have a legacy app that uses:
hibernate-3.5.5.jar hibernate-jpa-2.0-api-1.0.0.jar Spring 3.0.2 Tapestry 5.3.8 MySQL Tomcat 7.0.64
It has a serious issue with multiple users updating the same table row at the same time and loosing the first update. Basically user A says "I want to own the record" (place my id in the record) and user B says "I want to own the record" the code being processed takes a bit of time. So user A gets it and then user B doesn't notice that user A had it and so user B gets it when he shouldn't have because user A already had it.
I've tried using:
@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
on the Entity class for the table and watching the SQL that hibernate produces it never adds the table columns to the SQL update statement. It simply has update ... where id=?.
I've also tried adding a version column to the table in question and the Entity class and annotated the field with
@Version.
This has exactly the same effect as above, nothing in the generated SQL is using the version column. It never gets incremented either.
I'm guessing that I'm missing something with setting this up or there is something about the way the app uses hibernate that is keeping this from working as everything I have read says it should "Just Work".
appContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<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-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- Configurer that replaces ${...} placeholders with values from a properties
file -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:app_jdbc.properties"/>
</bean>
<!-- Message source for this context, loaded from localized files -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>app_app</value>
<value>app_env</value>
<value>pdf</value>
</list>
</property>
</bean>
<!-- Define data source -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>${jdbc.driverClassName}</value>
</property>
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="username">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
<property name="defaultAutoCommit">
<value>${jdbc.autoCommit}</value>
</property>
<property name="maxActive">
<value>${dbcp.maxActive}</value>
</property>
<property name="maxWait">
<value>${dbcp.maxWait}</value>
</property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="annotatedClasses">
<list>
...
<value>company.app.domain.Overtime</value>
...
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
<prop key="hibernate.query.substitutions">${hibernate.query.substitutions}</prop>
</props>
</property>
</bean>
<!-- Transaction manager for a single Hibernate SessionFactory (alternative
to JTA) -->
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<!-- regular beans -->
<bean id="baseDao" class="vive.db.BaseHbDao">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
...
<bean id="overtimeDao" class="company.app.dataaccess.OvertimeDao">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
...
<!-- service beans -->
<bean id="appService" class="company.app.services.AppService">
<property name="baseDao"><ref local="baseDao"/></property>
...
</bean>
<!-- transaction advice -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* company.app.services.*Service.*(..))" />
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice" />
</aop:config>
</beans>
Overtime Entity class:
package company.app.domain;
import java.util.Date;
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.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.OptimisticLockType;
@Entity
@Table(name = "over_time")
@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
public class Overtime implements java.io.Serializable {
private static final long serialVersionUID = 7263309927526074109L;
@Id
@GeneratedValue(generator = "ot_gen")
@GenericGenerator(name = "ot_gen", strategy = "hilo", parameters = {
@Parameter(name = "table", value = "unique_key"), @Parameter(name = "column", value = "next_hi"),
@Parameter(name = "max_lo", value = "99") })
private Integer id;
@Deprecated
@Column(name = "from_time")
private Date fromTime;
@Deprecated
@Column(name = "to_time")
private Date toTime;
@Column(name = "fm_dttm")
private Long fromDttm;
@Column(name = "to_dttm")
private Long toDttm;
@Column(name = "post_dttm")
private Long postDttm;
private String dow;
private String shift;
@Column(name = "sub_groups")
private String subGroups;
@Column(name = "created_by")
private String createdBy;
@Column(name = "signed_up_by")
private String signedUpBy;
@Column(name = "signed_up_via")
private String signedUpVia;
@Column(name = "date_signed_up")
private Date dateSignedUp;
@Column(name = "signed_up_by_partner_username")
private String signedUpByPartnerUsername;
@Column(name = "signed_up_by_partner_ot_refno")
private String signedUpByPartnerOtRefNo;
private String comment;
private Integer status;
@Column(name = "title_abbrev")
private String titleAbbrev;
@Column(name = "record_status")
private String recordStatus;
@Column(name = "ref_no")
private String refNo;
@Column(name = "ref_id")
private String refId;
@Column(name = "misc_notes")
private String miscNotes;
@Column(name = "sends_notif_upon_posting")
private Boolean sendsNotificationUponPosting;
@Column(name = "notify_post_person_when_filled")
private Boolean notifyPostPersonWhenFilled;
@Column(name = "notify_others_when_filled")
private Boolean notifyOthersWhenFilled;
@Column(name = "vehicle_needed")
private Boolean vehicleNeeded;
@Column(name = "agency_id")
private Integer agencyId;
@Column(name = "schedule_id")
private Integer scheduleId;
@Column(name = "post_date")
private Date postDate;
@Column(name = "enrollment_opens_at")
private Date enrollmentOpensAt;
@Column(name = "enrollment_closes_at")
private Date enrollmentClosesAt;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "class_id")
private OvertimeClass overtimeClass;
public Overtime() {
}
//getters and setters
}
The Tapestry page class where the user tries to sign up for the overtime:
package company.app.pages;
import java.io.*;
import java.text.MessageFormat;
import java.util.*;
import org.apache.tapestry5.StreamResponse;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SessionState;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestGlobals;
import org.hibernate.StaleObjectStateException;
import org.slf4j.Logger;
import org.springframework.transaction.annotation.Transactional;
import vive.util.*;
import company.t5ext.LabelValueSelectModel;
import company.t5ext.components.DateTimeField;
import company.app.*;
import company.app.domain.*;
import company.app.services.CacheService;
import company.app.services.AppService;
import company.app.comparator.OtComparator;
@RequiresLogin
public class ListPostedOvertime {
@SessionState
@Property
private AppSessionState visit;
@Inject
private RequestGlobals requestGlobals;
@Inject
@Property
private AppService appService;
@Inject
private Request request;
void setupRender() {
...
}
// this method handle the case when a user tries to sign up for an overtime slot
void onSignUp(Integer overtimeId) {
// check to see if the OT has been deleted or modified or signed-up
Overtime ot = (Overtime)appService.getById(Overtime.class, overtimeId);
if (ot == null) {
visit.setOneTimeMessage("The overtime has already been deleted.");
return;
}
if (ot.getStatus() != null && ot.getStatus() != AppConst.OT_NEW) {
visit.setOneTimeMessage("The overtime has already been signed up. Please choose a different one to sign up.");
return;
}
...
try {
appService.validateOvertimeForUser(agency, user, ot);
appService.handleSignUpOvertime(agency, user, ot);
// log activity
String what = "Signed up for overtime " + ot.getRefNo() + ".";
appService.logActivity(user, AppConst.LOG_OVERTIME, what);
} catch(StaleObjectStateException e) {
visit.setOneTimeMessage("The overtime record has been changed by another user, please try again.");
return;
} catch(Exception e) {
visit.setOneTimeMessage(e.getMessage());
return;
}
...
}
}
The AppService class that is used by the Tapestry page to update the overtime record:
package company.app.services;
import java.io.Serializable;
import java.util.*;
import java.text.DecimalFormat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.LockMode;
import org.springframework.context.support.ResourceBundleMessageSource;
import vive.db.BaseHbDao;
import vive.util.*;
import company.app.*;
import company.app.comparator.LeaveRequestComparator;
import company.app.comparator.UserOtInterestComparator;
import company.app.dataaccess.*;
import company.app.domain.*;
public class AppService
{
private Log log = LogFactory.getLog(this.getClass().getName());
private BaseHbDao baseDao;
private OvertimeDao otDao;
private MiscDao miscDao;
private ResourceBundleMessageSource msgSource;
/**
* Default constructor.
*/
public AppService() {
}
public void save(Object item) {
if (item != null) {
baseDao.save(item);
}
}
public void update(Object item) {
if (item != null) {
baseDao.update(item);
}
}
public void saveOrUpdate(Object item) {
if (item != null) {
baseDao.saveOrUpdate(item);
}
}
public void saveOrUpdateAll(Collection col) {
if (col != null) {
baseDao.saveOrUpdateAll(col);
}
}
public void delete(Object item) {
if (item != null) {
baseDao.delete(item);
}
}
public void deleteAll(Collection col) {
if (col != null) {
baseDao.deleteAll(col);
}
}
public Object getById(Class clazz, Serializable id) {
return baseDao.get(clazz, id);
}
public Object getById(Class clazz, Serializable id, LockMode lockMode) {
return baseDao.get(clazz, id, lockMode);
}
public void validateOvertimeForUser(Agency agency, User user, Overtime ot) throws Exception {
validateOvertimeForUser(agency.getId(), agency, user, ot);
}
public void validateOvertimeForUser(AgencyLite agency, User user, Overtime ot) throws Exception {
validateOvertimeForUser(agency.getId(), agency, user, ot);
}
public void handleSignUpOvertime(AgencyBase agency, User user, Integer otId) {
Overtime ot = (Overtime)getById(Overtime.class, otId);
handleSignUpOvertime(agency, user, ot);
}
public void handleSignUpOvertime(AgencyBase agency, User user, Overtime ot) {
handleSignUpOvertime(agency, user, ot, 1.0d);
}
public void handleSignUpOvertime(AgencyBase agency, User user, Integer otId, Double ptsPerOt) {
Overtime ot = (Overtime)getById(Overtime.class, otId);
handleSignUpOvertime(agency, user, ot, ptsPerOt);
}
public void handleSignUpOvertime(AgencyBase agency, User user, Overtime ot, Double ptsPerOt) {
handleSignUpOvertime(agency, user, ot, ptsPerOt, null, null);
}
public void handleSignUpOvertime(AgencyBase agency, User user, Overtime ot, Double ptsPerOt, String viaUsername, String viaName) {
Date today = new Date();
boolean isOtConfirmRequired = AppUtil.isTrue(agency.getOtConfirmRequired());
Integer otConfirmThreshold = 0;
if (agency.getOtConfirmThreshold() != null) {
otConfirmThreshold = agency.getOtConfirmThreshold();
}
long otInDays = (ot.getFromDttm() - today.getTime()) / AppConst.MILLIS_IN_DAY;
ot.setSignedUpBy(user.getUsername());
ot.setDateSignedUp(today);
ot.setSignedUpVia(viaUsername);
if (isOtConfirmRequired && otInDays >= otConfirmThreshold) {
ot.setStatus(AppConst.OT_PDG);
} else {
ot.setStatus(AppConst.OT_FIN);
}
saveOrUpdate(ot);
user.setLastOtSignupDate(today);
user.setPoints(AppUtil.addPoints(ptsPerOt, user.getPoints()));
saveOrUpdate(user);
...
// email notification sent from caller
}
...
}
The base class for all of the DAO classes:
package vive.db;
import java.io.Serializable;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.HibernateException;
import org.hibernate.type.Type;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import vive.XException;
import vive.util.XUtil;
/**
* The superclass for hibernate data access object.
*/
public class BaseHbDao extends HibernateDaoSupport implements BaseHbDaoInterface
{
private Log log;
public BaseHbDao() {
super();
log = LogFactory.getLog(getClass());
}
...
/**
* Save or update an object.
*/
public void saveOrUpdate(Object obj) {
getHibernateTemplate().saveOrUpdate(obj);
}
public void save(Object obj) {
getHibernateTemplate().save(obj);
}
public void update(Object obj) {
getHibernateTemplate().update(obj);
}
/**
* Delete an object.
*/
public void delete(Object obj) {
getHibernateTemplate().delete(obj);
}
/**
* Retrieve an object of the given id, null if it does not exist.
* Similar to "load" except that an exception will be thrown for "load" if
* the given record does not exist.
*/
public Object get(Class clz, Serializable id) {
return getHibernateTemplate().get(clz, id);
}
public Object get(Class clz, Serializable id, LockMode lockMode) {
return getHibernateTemplate().get(clz, id, lockMode);
}
...
public void flush() {
getHibernateTemplate().flush();
}
/**
* Retrieve a HB session.
* Make sure to release it after you are done with the session by calling
* releaseHbSession.
*/
public Session getHbSession() {
try {
return getSession();
} catch (Exception e) {
return null;
}
}
/**
* Release a HB Session
*/
public void releaseHbSession(Session sess) {
releaseSession(sess);
}
}
Upvotes: 2
Views: 4781
Reputation: 131
Okay I got it working!
First, I'm using the @Version annotation so I added a version column to the table with the problem.
alter table over_time add version INT(11) DEFAULT 0;
Second, add the Version annotation and member to the Entity class:
public class Overtime implements java.io.Serializable {
private static final long serialVersionUID = 7263309927526074109L;
@Id
@GeneratedValue(generator = "ot_gen")
@GenericGenerator(name = "ot_gen", strategy = "hilo", parameters = {
@Parameter(name = "table", value = "unique_key"), @Parameter(name = "column", value = "next_hi"),
@Parameter(name = "max_lo", value = "99") })
private Integer id;
@Version
@Column(name = "version")
private int version;
...
When I tried this the first couple of times I was using an Integer object not an int primitive for the version member of the class. I THINK this was the issue.
Also make sure that the other hibernate specific annotation is NOT on the entity class:
@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
Third, the exception that gets thrown is not what any of the web sites I've read say it should be, so let's catch the one that really is thrown in the Tapestry page class that handles the user signing up for the overtime record.
void onSignUp(Integer overtimeId) {
// check to see if the OT has been deleted or modified or signed-up
Overtime ot = (Overtime)appService.getById(Overtime.class, overtimeId);
if (ot == null) {
visit.setOneTimeMessage("The overtime has already been deleted.");
return;
}
if (ot.getStatus() != null && ot.getStatus() != AppConst.OT_NEW) {
visit.setOneTimeMessage("The overtime has already been signed up. Please choose a different one to sign up.");
return;
}
...
try {
appService.validateOvertimeForUser(agency, user, ot);
appService.handleSignUpOvertime(agency, user, ot);
// log activity
String what = "Signed up for overtime " + ot.getRefNo() + ".";
appService.logActivity(user, AppConst.LOG_OVERTIME, what);
} catch(HibernateOptimisticLockingFailureException x) {
visit.setOneTimeMessage("The overtime record has been changed by another user, please try again.");
return;
} catch(Exception e) {
visit.setOneTimeMessage(e.getMessage());
return;
}
...
Upvotes: 1