Grails 5.3 GSP问题:语法错误与循环加载缓慢求助
两个Grails升级及性能问题的解决方案
问题1:下拉框onchange事件抛出语法错误(Uncaught Syntax Error: expected expression got '}')
问题分析
升级到Grails 5.3后,remoteFunction的输出格式与GSP表达式解析逻辑发生了变化。原代码在onSuccess中直接拼接多个remoteFunction调用,导致生成的JavaScript出现引号不匹配、大括号错位的语法问题——旧版本Grails对这种嵌套拼接的兼容性更好,但新版本模板引擎对字符串转义和表达式拼接的校验更严格。另外原代码中存在DOM ID拼写错误:ControlCroup应为ControlGroup,虽不是直接报错原因,但会导致后续DOM操作失效。
解决方案
避免在remoteFunction参数中嵌套另一个remoteFunction,将多步异步操作封装为独立JS函数,再在onSuccess中调用:
- 在页面
<script>块定义处理函数:
function handleResponseSuccess(beanId) { jQuery("#reviewSaveNotice").stop(true).fadeIn().delay(1500).fadeOut(); jQuery('#questionResponse' + beanId + 'ControlGroup').removeClass('error'); updateQuestionScore(data); // 调用评分渲染接口 ${remoteFunction(controller: 'review', action: 'renderScore', id: "${bean.reviewSection.review.id}", update: 'scoreTotal')}; // 调用章节评分渲染接口 ${remoteFunction(controller: 'review', action: 'renderSectionScore', id: "${bean.reviewSection.id}", update: "sectionScore${bean.reviewSection.id}")}; }
- 修改
g:select的onchange属性,简化remoteFunction配置:
<div id="questionResponse${bean?.id}ControlGroup" class="control-group"> <div class="controls"> <g:hiddenField name="reviewQuestionResponse.${bean?.id}.id" value="${bean?.id}" /> <g:select class="input-block-level" name="reviewQuestionResponse.${bean?.id}.response" from="${com.moo.qwr.enums.QuestionResponseType.values()}" value="${value}" onchange="${remoteFunction(controller: 'reviewQuestionResponse', action: 'update', id: "${bean?.id}", params: '\'response=\' + this.value', onSuccess: "handleResponseSuccess('${bean?.id}')", onFailure: "jQuery('#questionResponse${bean?.id}ControlGroup').addClass('error')" )}" /> </div>
问题2:编辑表单加载极慢(8-10分钟),创建表单正常
问题分析
核心原因是N+1查询问题:
reviewInstance.fields是hasMany关联的SortedSet,默认懒加载,g:each遍历时每次迭代都会触发一次数据库查询加载单个ReviewField实例;- 如果
ReviewField的渲染模板中还访问了其关联对象,会触发更多嵌套查询,累积大量数据库请求导致耗时剧增; - 创建表单时
reviewInstance.fields为空,不会触发这些查询,因此加载速度正常。
另外,SortedSet的内存排序操作(未通过数据库排序)在数据量较大时也会增加耗时。
解决方案
- 优化关联查询,消除N+1
在获取reviewInstance的Controller/Service方法中,使用fetch join一次性加载关联数据:
// 示例:ReviewController的edit方法 def edit(Long id) { def reviewInstance = Review.createCriteria().get { eq('id', id) fetchMode('fields', FetchMode.JOIN) // 一次性加载fields集合 // 若ReviewField还有关联对象,需在此添加对应fetchMode } [reviewInstance: reviewInstance] }
也可在领域类中配置批量加载:
// Review领域类 static mapping = { fields batchSize: 50 // 批量加载减少查询次数 }
- 优化SortedSet排序逻辑
确保排序在数据库层面完成,避免内存排序:
// Review领域类 static mapping = { fields sort: 'sortField', order: 'asc' // sortField为ReviewField的排序字段 }
- 简化模板渲染逻辑
- 避免在模板中调用
ClassUtils.getShortNameAsProperty(it?.class)这类反射操作,提前在Service中处理好模板路径并传入; - 检查模板内是否有重复查询、耗时计算,尽量将数据预处理后传入模板。
- 开启日志排查
在logback.groovy中开启Hibernate SQL日志,确认是否存在大量重复查询:
logger('org.hibernate.SQL', DEBUG, ['STDOUT'], false) logger('org.hibernate.type.descriptor.sql.BasicBinder', TRACE, ['STDOUT'], false)
内容的提问来源于stack exchange,提问作者Daniel Geiselman
相关产品推荐
相关产品推荐

