【问题标题】:Usage of @Transactional with Spring Data custom methods将 @Transactional 与 Spring Data 自定义方法一起使用
【发布时间】:2016-08-15 03:58:39
【问题描述】:

我正在使用 Spring Data(通过 Spring Boot 1.3.3)。我所有的存储库都有一个自定义方法来获取主键。例如:

@Transactional(readOnly=true)
@Repository
public interface UserRepository extends CrudRepository<User, UserId>, UserRepositoryCustom {
  User findByUsername(String username);
}

public interface UserRepositoryCustom {
  UserId nextId();
}

public class UserRepositoryImpl implements UserRepositoryCustom {
  public UserId nextId() {
    return new UserId( UUID.randomUUID() );
  }
}

@Transactional 的使用在这里正确吗?或者我是否还需要将@Transactional 添加到UserRepositoryImpl(可能设置了只读)?

我问的原因是因为我无法解释ObjectOptimisticLockingFailureException

org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class 
[com.company.project.domain.Game] with identifier [GameId{id=7968c30b-838f-424c-bfef-838de7028def}]: 
optimistic locking failed; nested exception is 
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction 
(or unsaved-value mapping was incorrect) : [com.company.project.domain.Game#GameId{id=7968c30b-838f-424c-bfef-838de7028def}]

这发生在 JMeter 测试期间。尽管调用的方法不会以任何方式更改 Game 实体。

我已将此添加到我的 Game 实体中以进行调试:

@PreUpdate
public void preUpdate() {
    System.out.println("GAME UPDATED!! version = " + version);
    Thread.dumpStack();
}

这会给出几次类似这样的堆栈跟踪:

java.lang.Exception: Stack trace
    at java.lang.Thread.dumpStack(Thread.java:1329)
    at com.company.project.domain.Game.preUpdate(Game.java:85)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.hibernate.jpa.event.internal.jpa.EntityCallback.performCallback(EntityCallback.java:47)
    at org.hibernate.jpa.event.internal.jpa.CallbackRegistryImpl.callback(CallbackRegistryImpl.java:112)
    at org.hibernate.jpa.event.internal.jpa.CallbackRegistryImpl.preUpdate(CallbackRegistryImpl.java:76)
    at org.hibernate.jpa.event.internal.core.JpaFlushEntityEventListener.invokeInterceptor(JpaFlushEntityEventListener.java:68)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.handleInterception(DefaultFlushEntityEventListener.java:342)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.scheduleUpdate(DefaultFlushEntityEventListener.java:293)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:160)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:231)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:102)
    at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:61)
    at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1227)
    at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1293)
    at org.hibernate.internal.QueryImpl.list(QueryImpl.java:103)
    at org.hibernate.jpa.internal.QueryImpl.list(QueryImpl.java:573)
    at org.hibernate.jpa.internal.QueryImpl.getSingleResult(QueryImpl.java:495)
    at org.hibernate.jpa.criteria.compile.CriteriaQueryTypeQueryAdapter.getSingleResult(CriteriaQueryTypeQueryAdapter.java:71)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:206)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:78)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:100)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:91)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:462)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:440)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:131)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy141.findByUsername(Unknown Source)
    at com.company.project.service.UserServiceImpl.findByUsername(UserServiceImpl.java:117)
    at com.company.project.service.UserServiceImpl.subtractCredits(UserServiceImpl.java:143)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy154.subtractCredits(Unknown Source)
    at com.company.project.service.GameServiceImpl.subtractCreditsForPlacedShotsAndSave(GameServiceImpl.java:703)
    at com.company.project.service.GameServiceImpl.placeShotsOnGameWhenGameIsOpen(GameServiceImpl.java:641)
    at com.company.project.service.GameServiceImpl.placeShotsOnGame(GameServiceImpl.java:629)
    at com.company.project.service.GameServiceImpl.placeShots(GameServiceImpl.java:281)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy164.placeShots(Unknown Source)
    at com.company.project.controller.front.FrontGameController.placeShots(FrontGameController.java:180)

仅查看与我的应用相关的内容,您会看到:

java.lang.Exception: Stack trace
    at java.lang.Thread.dumpStack(Thread.java:1329)
    at com.company.project.domain.Game.preUpdate(Game.java:85)
    at com.company.project.service.UserServiceImpl.findByUsername(UserServiceImpl.java:117)
    at com.company.project.service.UserServiceImpl.subtractCredits(UserServiceImpl.java:143)
    at com.company.project.service.GameServiceImpl.subtractCreditsForPlacedShotsAndSave(GameServiceImpl.java:703)
    at com.company.project.service.GameServiceImpl.placeShotsOnGameWhenGameIsOpen(GameServiceImpl.java:641)
    at com.company.project.service.GameServiceImpl.placeShotsOnGame(GameServiceImpl.java:629)
    at com.company.project.service.GameServiceImpl.placeShots(GameServiceImpl.java:281)
    at com.company.project.controller.front.FrontGameController.placeShots(FrontGameController.java:180)

所以不知何故,findByUsername 似乎触发了对不相关实体 Game 的更新?

仅供参考:GameServiceImpl#placeShots 也有一个 @Transactional 注释。我也尝试在 Controller 方法上添加这样的注释,但这并没有改变任何东西。

【问题讨论】:

  • 查看关于 @Transactional 和特别是 Spring recommends that you only annotate concrete classes.... 的 spring 文档。 findByUserName 触发当前事务中已完成的自动刷新更新。出于某种原因,Hibernate 会触发此自动刷新,可能是因为用户和游戏之间存在关系。您是否在调用 findByUsername 之前修改了 Game 实体(甚至通过调用 setter)?
  • 我在Game的所有设置器中添加了日志记录,但没有显示,所以我没有更新它。
  • 您可以尝试启用show_sql 属性以显示哪些 SQL 被刷新并提高休眠日志记录级别

标签: java spring hibernate transactions spring-data


【解决方案1】:

问题不是在我使用@Transactional 时。

我使用的是自定义 Hibernate UserType,它使用 Jackson 库将对象存储为 JSON。 Game 对象有一个使用此 UserType 的字段。该字段的类没有实现equals()。结果,Hibernate 假定对象已更改,并在我的 Game 对象上发出保存。

正确实现equals()后,问题就消失了。

【讨论】:

    【解决方案2】:

    不要在接口上使用@Transactional。还要小心内部方法调用。

    http://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html#transaction-declarative-annotations

    提示 1:

    Spring 建议您只注释具体的类(和方法 具体类)与@Transactional 注释,而不是 注释接口。你当然可以把@Transactional 接口(或接口方法)上的注释,但这有效 仅当您使用基于界面的 代理。 Java注解不是继承自的事实 接口意味着如果您使用基于类的代理( proxy-target-class="true") 或基于编织的方面 ( mode="aspectj"),则交易设置不被识别 代理和编织基础设施,并且对象不会 包裹在事务代理中,这绝对是糟糕的。

    提示 2:

    在代理模式下(默认),只有外部方法调用 通过代理进来的被拦截。这意味着 自调用,实际上是目标对象调用的方法 目标对象的另一种方法,不会导致实际 即使调用的方法标记为 @事务。此外,代理必须完全初始化以提供 预期的行为,因此您不应在您的 初始化代码,即@PostConstruct。

    我不确定这是否会完全解决您的问题,但我认为这是朝着正确方向迈出的良好一步。

    在这些情况下,我通常会为 spring 启用调试级别日志记录(顺便说一下,使用大型应用程序祝你好运)并启用 mysql 上的常规日志。

    How to show the last queries executed on MySQL?

    然后尝试在 spring 中运行查询(首选调试器)并检查 mysql 日志。

    【讨论】:

    • 小额外没有。不要使用 com.company.project.service.x、com.company.project.domain.x 等打包方式。它会变得一发不可收拾,您的域将分散且难以使用。使用 com.company.project.game.service、com.company.project.game.domain、com.company.project.user.service、com.company.project.user.domain 等...
    猜你喜欢
    • 2012-05-10
    • 2016-08-31
    • 1970-01-01
    • 2014-10-21
    • 2011-12-31
    • 2014-03-10
    • 2019-07-31
    • 2017-03-07
    • 1970-01-01
    相关资源
    最近更新 更多