Hibernate 乐观锁 with/without 一个版本不工作

Hibernate Optimistic Lock with/without a Version not working

我有一个旧版应用程序使用:

hibernate-3.5.5.jar hibernate-jpa-2.0-api-1.0.0.jar Spring 3.0.2 挂毯 5.3.8 MySQL Tomcat7.0.64

多个用户同时更新同一 table 行并丢失第一个更新时存在严重问题。基本上用户 A 说 "I want to own the record"(将我的 ID 放在记录中),用户 B 说 "I want to own the record" 正在处理的代码需要一些时间。所以用户 A 得到了它,然后用户 B 没有注意到用户 A 拥有它,所以用户 B 在他不应该拥有它的时候得到了它,因为用户 A 已经拥有它。

我试过使用:

@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)

在 table 的实体 class 上观察休眠生成的 SQL 它从不将 table 列添加到 SQL 更新语句.它只是有 update ... where id=?.

我还尝试将版本列添加到有问题的 table 和实体 class 并用

注释该字段
@Version.

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

我猜我在设置时遗漏了一些东西,或者应用程序使用休眠的方式使它无法正常工作,因为我读过的所有内容都说它应该 "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>

加班实体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
}

用户尝试注册超时的 Tapestry 页面 class:

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页面更新超时记录使用的AppServiceclass:

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 class 的基础 class:

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 注释,所以我在有问题的 table 中添加了一个版本列。

alter table over_time add version INT(11) DEFAULT 0;

其次,为Entity添加Version注解和成员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;

...

当我最初几次尝试这个时,我使用的是 Integer 对象,而不是 class 的版本成员的 int 原语。我认为这是问题所在。

还要确保其他休眠特定注释不在实体上 class:

@org.hibernate.annotations.Entity(dynamicUpdate = true, optimisticLock = OptimisticLockType.ALL)

第三,抛出的异常不是我读过的任何网站所说的应该是的异常,所以让我们来看看 Tapestry 页面 class 中真正抛出的那个处理用户报名加班记录

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

...