You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Grails中调用save()后执行delete触发FlushMode.MANUAL只读错误

问题:Grails执行parent.save()后object.delete()抛出FlushMode.MANUAL只读模式异常

问题代码

服务中定义的delete方法:

def delete(object) {
  def parent = object.parent
  if (parent) {
    object.parent = null
    parent.removeFromX(object)
    parent.save()
  }
  object.delete(flush: true)
}

异常现象

当触发parent.save()后执行object.delete(flush: true),抛出以下错误:

Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.

若parent为null、不执行save()时,delete操作可正常完成。调试发现:执行save()前会话的FlushMode为AUTO,执行后变为MANUAL。项目使用了OSIV(Open Session In View)机制。

完整堆栈信息

org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.
        at org.springframework.orm.hibernate3.HibernateTemplate.checkWriteOperationAllowed(HibernateTemplate.java:1175)
        at org.springframework.orm.hibernate3.HibernateTemplate$25.doInHibernate(HibernateTemplate.java:839)
        at org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:406)
        at org.springframework.orm.hibernate3.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:374)
        at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:837)
        at org.springframework.orm.hibernate3.HibernateTemplate.delete(HibernateTemplate.java:833)
        at org.springframework.orm.hibernate3.HibernateOperations$delete.call(Unknown Source)
        at org.codehaus.groovy.grails.plugins.orm.hibernate.HibernatePluginSupport$_addBasicPersistenceMethods_closure78.doCall(HibernatePluginSupport.groovy:847)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:607)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
        at org.codehaus.groovy.runtime.metaclass.ClosureMetaMethod.invoke(ClosureMetaMethod.java:80)
        at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoMetaMethodSiteNoUnwrapNoCoerce.invoke(PogoMetaMethodSite.java:307)
        at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.call(PogoMetaMethodSite.java:63)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:124)
        at org.pih.warehouse.core.LocationService.deleteLocation(LocationService.groovy:716)
        at org.pih.warehouse.core.LocationService$$FastClassByCGLIB$$366d1ea5.invoke(<generated>)
        at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149)
        at org.springframework.aop.framework.Cglib2AopProxy$CglibMethodInvocation.invokeJoinpoint(Cglib2AopProxy.java:688)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
        at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:621)
        at org.pih.warehouse.core.LocationService$$EnhancerByCGLIB$$7d8a27ff.deleteLocation(<generated>)
        at org.pih.warehouse.core.LocationService$deleteLocation.call(Unknown Source)
        at org.pih.warehouse.core.LocationController$_closure6.doCall(LocationController.groovy:162)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:607)
        at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSite.invoke(PogoMetaMethodSite.java:225)
        at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.callCurrent(PogoMetaMethodSite.java:51)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:149)
        at org.pih.warehouse.core.LocationController$_closure6.doCall(LocationController.groovy)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:607)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1058)
        at groovy.lang.ExpandoMetaClass.invokeMethod(ExpandoMetaClass.java:1070)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:886)
        at groovy.lang.Closure.call(Closure.java:282)
        at groovy.lang.Closure.call(Closure.java:277)
        at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleAction(SimpleGrailsControllerHelper.java:368)
        at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.executeAction(SimpleGrailsControllerHelper.java:232)
        at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(SimpleGrailsControllerHelper.java:190)
        at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(SimpleGrailsControllerHelper.java:129)
        at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsController.handleRequest(SimpleGrailsController.java:73)
        at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:48)
        at org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(GrailsDispatcherServlet.java:292)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:719)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:644)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:549)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:70)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:70)

关键调试信息

确认FlushMode.MANUAL通过以下方法被设置:

public void setFlushMode(FlushMode flushMode) {
        this.errorIfClosed();
        this.checkTransactionSynchStatus();
        if (log.isTraceEnabled()) {
            log.trace("setting flush mode to: " + flushMode);
        }

        this.flushMode = flushMode;
    }

解决方案

方案1:显式声明方法为可写事务

Grails服务默认事务为只读模式,给delete方法添加@Transactional(readOnly = false)注解,确保整个操作在可写事务中执行,避免Hibernate将FlushMode切换为MANUAL:

import org.springframework.transaction.annotation.Transactional

@Transactional(readOnly = false)
def delete(object) {
  def parent = object.parent
  if (parent) {
    object.parent = null
    parent.removeFromX(object)
    parent.save()
  }
  object.delete(flush: true)
}

方案2:合并操作,移除显式parent.save()

在同一个会话/事务中,对parent的修改会在事务提交或会话flush时自动持久化,无需单独调用parent.save()。修改代码如下:

def delete(object) {
  def parent = object.parent
  if (parent) {
    object.parent = null
    parent.removeFromX(object)
    // 移除parent.save(),事务提交时自动同步修改
  }
  object.delete(flush: true)
}

此方法更符合Hibernate的持久化机制,减少不必要的显式flush操作。

方案3:手动重置FlushMode(不推荐)

若无法修改事务配置,可在parent.save()后手动将FlushMode改回AUTO,再执行delete操作:

import org.hibernate.FlushMode
import org.springframework.orm.hibernate3.SessionFactoryUtils
import org.springframework.transaction.support.TransactionSynchronizationManager

def delete(object) {
  def parent = object.parent
  if (parent) {
    object.parent = null
    parent.removeFromX(object)
    parent.save()
    // 手动重置会话FlushMode
    def session = SessionFactoryUtils.getSession(sessionFactory, false)
    if (session) {
      session.flushMode = FlushMode.AUTO
    }
  }
  object.delete(flush: true)
}

该方法属于临时 workaround,可能引发其他会话状态问题,优先推荐方案1或方案2。

原因分析

问题根源在于Grails默认的只读事务设置:当在只读事务中执行写操作(如parent.save()),Hibernate会将会话FlushMode切换为MANUAL以阻止后续写操作,导致后续的object.delete()因处于只读模式而抛出异常。显式声明可写事务或利用Hibernate自动持久化机制,即可避免此问题。

内容的提问来源于stack exchange,提问作者sunny

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 21:02:53