Mysql 在版本不工作的情况下休眠乐观锁

Mysql 在版本不工作的情况下休眠乐观锁,mysql,spring,hibernate,tapestry,optimistic-locking,Mysql,Spring,Hibernate,Tapestry,Optimistic Locking,我有一个传统应用程序,它使用: 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 它有一个严重的问题,即多个用户同时更新同一个表行并丢失第一次更新。基本上,用户A说“我想拥有记录”(将我的id放在记录中),用户B说“我想拥有记录”。正在处理的代码需要一些时间。所以用户A得到了它,然后用户B没有注意到用户A拥有它,所以用户B在他不应该拥有它的时候得到

我有一个传统应用程序,它使用:

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

它有一个严重的问题,即多个用户同时更新同一个表行并丢失第一次更新。基本上,用户A说“我想拥有记录”(将我的id放在记录中),用户B说“我想拥有记录”。正在处理的代码需要一些时间。所以用户A得到了它,然后用户B没有注意到用户A拥有它,所以用户B在他不应该拥有它的时候得到了它,因为用户A已经拥有了它

我试过使用:

@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
在表的实体类上,并观察hibernate生成的SQL,它从不将表列添加到SQL update语句中。它只是有更新。。。其中id=

我还尝试向相关表和实体类添加一个版本列,并用

@Version.
这与上面的效果完全相同,生成的SQL中没有任何内容使用版本列。它也永远不会递增

我猜我在设置这个时遗漏了一些东西,或者是应用程序使用hibernate的方式让它无法工作,因为我读到的所有东西都说它应该“正常工作”

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>
用户尝试注册加班的Tapestry页面类:

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;
    }

    ...
  }

}
Tapestry页面用于更新加班记录的AppService类:

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
  }

  ...
}
所有DAO类的基类:

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);
  }

}

好了,我成功了

首先,我使用@Version注释,所以我在表中添加了一个版本列来解决问题

alter table over_time add version INT(11) DEFAULT 0;
其次,将版本注释和成员添加到实体类:

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
}
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;

...
@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
前几次尝试时,我使用的是一个整数对象,而不是类的version成员的int原语。我认为这就是问题所在

还要确保实体类上没有其他特定于hibernate的注释:

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
}
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;

...
@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)
第三,抛出的异常不是我读过的任何网站所说的异常,所以让我们来看看Tapestry page类中真正抛出的异常,该类处理用户注册超时记录

  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;
    }

...

还要注意,以前,当@Version不起作用时,我将@Version注释放在Version字段的getter方法上。这次我把它放到了字段中,甚至没有添加getter/setter方法。最终,这可能是真正的问题,而不是使用整数对象而不是原语。