Grails中调用save()后执行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

