带/不带版本的Hibernate Optimistic Lock无法正常工作

时间:2016-04-06 17:12:51

标签: mysql spring hibernate tapestry optimistic-locking

我有一个使用的遗留应用:

冬眠-3.5.5.jar 冬眠-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)

在表的Entity类上,并观察hibernate生成的SQL,它永远不会将表列添加到SQL更新语句中。它只是更新...其中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>

加班实体类:

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

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

}

1 个答案:

答案 0 :(得分:0)

好的,我搞定了!

首先,我正在使用@Version注释,因此我在表格中添加了一个版本列,并提出了问题。

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

其次,将Version注释和成员添加到Entity类:

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对象而不是类的版本成员的int原语。我认为这是个问题。

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

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

第三,被抛出的异常不是我读过的任何网站所说的应该是,所以让我们抓住Tapestry页面类中真正抛出的那个,它处理用户注册加班记录。

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

...