Grails 4中如何在HQL中嵌入orderBy字符串实现排序?
在Grails 4中实现HQL动态排序的解决方案
问题原因
- Grails 4增强了HQL安全校验,禁止GString(字符串拼接)与位置参数混合使用,防止SQL注入风险,所以你原来直接拼接
sortp的方式会触发安全报错。 - 你尝试用命名参数传递排序语句无效,因为HQL的
order by子句中,参数会被当作字符串值处理,而非字段标识符,数据库无法识别为排序规则。
解决方案
方案1:安全拼接排序子句(白名单校验)
通过校验排序字段和方向的合法性,避免注入风险后再拼接HQL,这是最接近你原有代码的方式:
// 定义允许排序的字段白名单,仅包含业务允许的字段,杜绝注入 def allowedSortFields = [ 'id', 'bibLabelPrinted', 'raceParticipant.bibNumber', 'bibLabelCollected', 'raceParticipant.firstName', 'raceParticipant.lastName', 'raceParticipant.gender', 'race.name', 'dateCreated', 'status' // 补充你需要支持的其他字段 ] // 处理默认排序和参数校验 def sortField = params.sort ?: 'id' def sortOrder = (params.order ?: 'asc').toLowerCase() // 校验参数合法性,非法参数重置为默认值 if (!allowedSortFields.contains(sortField)) { sortField = 'id' } if (!['asc', 'desc'].contains(sortOrder)) { sortOrder = 'asc' } // 构建安全的排序子句 def sortClause = "order by r.${sortField} ${sortOrder}" // 执行查询,使用命名参数传递条件值 def allRegsData = RaceRegistration.executeQuery(""" select r.id, r.bibLabelPrinted, r.raceParticipant.bibNumber, r.bibLabelCollected, r.raceParticipant.firstName, r.raceParticipant.lastName, r.raceParticipant.gender, r.race.name, rt.name, r.raceParticipant.ageDate, ad.genderOption, ad.minAge, ad.maxAge, sd.name, sm.encodedMark, p.name, p.abbreviation, ps.name, r.dateCreated, r.status, r.race.id, re.invoice.id, io.status, r.notes from InventoryOrder io inner join io.product p inner join io.productSize ps right outer join io.registration r left outer join r.registrationEntry re left outer join r.specialDivision sd left outer join r.ageDivision ad left outer join r.team rt left outer join r.seedMark sm where r.compositeEvent = :rg ${sortClause} """, [rg: raceGroup])
方案2:使用Grails Criteria API(更推荐)
Criteria API是Grails官方推荐的查询方式,无需手动拼接HQL,天然安全,代码可读性更强:
// 同样定义排序字段白名单 def allowedSortFields = [ 'id', 'bibLabelPrinted', 'raceParticipant.bibNumber', 'bibLabelCollected', 'raceParticipant.firstName', 'raceParticipant.lastName', 'raceParticipant.gender', 'race.name', 'dateCreated', 'status' ] def allRegsData = RaceRegistration.createCriteria().list { // 构建关联关系,对应原HQL中的join createAlias('registration', 'r') createAlias('r.registrationEntry', 're', JoinType.LEFT_OUTER_JOIN) createAlias('r.specialDivision', 'sd', JoinType.LEFT_OUTER_JOIN) createAlias('r.ageDivision', 'ad', JoinType.LEFT_OUTER_JOIN) createAlias('r.team', 'rt', JoinType.LEFT_OUTER_JOIN) createAlias('r.seedMark', 'sm', JoinType.LEFT_OUTER_JOIN) createAlias('r.race', 'race', JoinType.LEFT_OUTER_JOIN) createAlias('r.raceParticipant', 'raceParticipant', JoinType.LEFT_OUTER_JOIN) createAlias('inventoryOrder', 'io', JoinType.RIGHT_OUTER_JOIN) createAlias('io.product', 'p', JoinType.INNER_JOIN) createAlias('io.productSize', 'ps', JoinType.INNER_JOIN) // 查询条件 eq('r.compositeEvent', raceGroup) // 处理排序 if (params.sort && allowedSortFields.contains(params.sort)) { def sortOrder = (params.order ?: 'asc').toLowerCase() order("r.${params.sort}", sortOrder) } else { // 默认排序 order('r.id', 'asc') } // 投影查询,对应原HQL的select字段 projections { property('r.id') property('r.bibLabelPrinted') property('r.raceParticipant.bibNumber') property('r.bibLabelCollected') property('r.raceParticipant.firstName') property('r.raceParticipant.lastName') property('r.raceParticipant.gender') property('race.name') property('rt.name') property('r.raceParticipant.ageDate') property('ad.genderOption') property('ad.minAge') property('ad.maxAge') property('sd.name') property('sm.encodedMark') property('p.name') property('p.abbreviation') property('ps.name') property('r.dateCreated') property('r.status') property('race.id') property('re.invoice.id') property('io.status') property('r.notes') } }
关键说明
- 必须对
params.sort和params.order做合法性校验:只允许业务范围内的字段和asc/desc方向,防止SQL注入。 - 排序字段不能通过命名参数传递,因为HQL会将其解析为字符串值,而非字段标识符,这是你之前尝试失败的核心原因。
内容的提问来源于stack exchange,提问作者kofhearts
相关产品推荐
相关产品推荐

