JPA执行原生分页语句,公共dao层方法封装
·
说明
使用方法要3个步骤
1、你的 dao 层实现继承这个方法即可
2、实现jakarta.persistence.Tuple 类转换成你的实体方法
3、按照公共方法使用样例,调用公共 BaseNativeRepository
注:ExamInfoMapper.INSTANCE::toPageVo 见3
1 公共方法的使用
1、paramMap 可传递 param 没有的默认参数,例如用户的id、部门、角色等
2、公平方法内还提供不用传递countsql 方法,我这里是为了性能更高而传递了 countsql
package com.office.exam.dao.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.office.exam.dao.ExamInfoRepositoryExt;
import com.office.exam.entity.param.ExamInfoParam;
import com.office.exam.entity.vo.ExamInfoPageVo;
import com.office.exam.mapper.ExamInfoMapper;
import com.server.common.dataBase.dao.BaseNativeRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;
@Repository
public class ExamInfoRepositoryExtImpl extends BaseNativeRepository implements ExamInfoRepositoryExt {
@Override
public Page<ExamInfoPageVo> findPageByParam(ExamInfoParam param, Pageable pageable) {
Map<String, Object> paramMap = new HashMap<>();
String dataSql = buildSqlAndParams(param, false, paramMap);
String countSql = buildSqlAndParams(param, true, paramMap);
return nativeQuery(dataSql, countSql, paramMap, pageable, ExamInfoMapper.INSTANCE::toPageVo);
}
private String buildSqlAndParams(ExamInfoParam param, boolean isCount, Map<String, Object> sqlParams) {
StringBuilder sql = new StringBuilder();
if (isCount) {
sql.append("SELECT COUNT(1) FROM exam_info ei WHERE 1=1 ");
} else {
sql.append("""
SELECT ei.id,ei.exam_name,etp.title AS paper_name,etp.duration,etp.score
FROM exam_info ei
LEFT JOIN exam_paper etp ON ei.paper_id = etp.id
LEFT JOIN exam_participant ep ON ep.exam_id = ei.id
WHERE 1=1
""");
}
// 2 添加参数
if (StrUtil.isNotBlank(param.getExamName())) {
sql.append(" AND ei.exam_name LIKE :examName ");
sqlParams.put("examName", "%" + param.getExamName() + "%");
}
return sql.toString();
}
}
2 公共方法
BaseNativeRepository 方法,
理论上:nativeQuery(String dataSql, Map<String, Object> paramMap, Class resultClass) 中的resultClass 结果实体类有很多限制:
1)必须是没有继承的实体,且实现全部构建参数方法
2)查询返回对象属性数量,必须与实体类一直
3)实体类不能包含LocalDateTime 不支持默认类型转换的类
由于各种限制,所以推荐使用带外部类型转换方法 Function<Tuple, T> mapper
package com.server.common.dataBase.dao;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import jakarta.persistence.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public abstract class BaseNativeRepository {
@PersistenceContext
protected EntityManager entityManager;
/**
* 通用分页查询(自动生成 countSql)
*
* @param dataSql 数据sql:不带分页
* @param paramMap 数据sql 和分页sql公共的参数
* @param pageable 分页参数
* @param mapper 单个类型转换
* @param <T> 结果类型
* @return 分页结果
*/
protected <T> Page<T> nativeQuery(String dataSql, Map<String, Object> paramMap, Pageable pageable, Function<Tuple, T> mapper) {
// 自动生成 countSql
String countSql = "SELECT COUNT(*) FROM (" + dataSql + ") AS count_query";
return nativeQuery(dataSql, countSql, paramMap, pageable, mapper);
}
/**
* 通用分页查询(外部传入 countSql,性能更优)
*
* @param dataSql 数据sql:不带分页
* @param countSql 数量sql
* @param paramMap 数据sql 和分页sql公共的参数
* @param pageable 分页参数
* @param mapper 单个类型转换
* @param <T> 结果类型
* @return 分页结果
*/
protected <T> Page<T> nativeQuery(String dataSql, String countSql, Map<String, Object> paramMap, Pageable pageable, Function<Tuple, T> mapper) {
// 1 执行 count 查询,获取总记录数
long total = nativeCountQuery(countSql, paramMap);
// 2 如果 total 为 0,直接返回空分页
if (total == 0) {
return new PageImpl<>(ListUtil.empty(), pageable, 0);
}
// 3 创建主查询对象(返回 Tuple)
Query dataQuery = entityManager.createNativeQuery(dataSql, Tuple.class);
// 4 设置参数
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(dataQuery::setParameter);
}
// 5 设置分页参数(offset、limit)
dataQuery.setFirstResult(Convert.toInt(pageable.getOffset()));
dataQuery.setMaxResults(pageable.getPageSize());
// 6 执行主查询
List<Tuple> resultList = dataQuery.getResultList();
// 7 转换结果为 DTO / VO
List<T> mapped = resultList.stream()
.map(mapper)
.toList();
// 8 返回分页结果
return new PageImpl<>(mapped, pageable, total);
}
/**
* 通用列表查询
* 自定义结果类型映射
*
* @param dataSql 数据查询语句
* @param paramMap 参数
* @param mapper 类型转换
* @param <T> 结果类型
* @return 结果
*/
protected <T> List<T> nativeQuery(String dataSql, Map<String, Object> paramMap, Function<Tuple, T> mapper) {
// 1 创建主查询对象(返回 Tuple)
Query dataQuery = entityManager.createNativeQuery(dataSql, Tuple.class);
// 2 设置参数
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(dataQuery::setParameter);
}
// 3 执行主查询
List<Tuple> resultList = dataQuery.getResultList();
return resultList.stream()
.map(mapper)
.toList();
}
/**
* 通用列表查询,只能用于简单类型场景,例如 1)必须是没有继承的实体,且实现全部构建参数方法;
* 2)查询返回对象属性数量,必须与实体类一致;3)实体类不能包含LocalDateTime 不支持默认类型转换的类
*
* @param dataSql 数据查询语句
* @param paramMap 参数
* @param resultClass 返回类型
* @param <T> 结果类型
* @return 结果
*/
protected <T> List<T> nativeQuery(String dataSql, Map<String, Object> paramMap, Class<T> resultClass) {
// 1 创建主查询对象(返回 Tuple)
Query dataQuery = entityManager.createNativeQuery(dataSql, resultClass);
// 2 设置参数
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(dataQuery::setParameter);
}
// 执行查询
return dataQuery.getResultList();
}
/**
* 查询单个值(getSingleResult)
*
* @param sql SQL 语句
* @param paramMap 参数
* @return 查询结果,没查到返回 null
*/
protected <T> T nativeQuerySingle(String sql, Map<String, Object> paramMap, Function<Tuple, T> mapper) {
Query query = entityManager.createNativeQuery(sql, Tuple.class);
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(query::setParameter);
}
try {
Tuple singleResult = (Tuple) query.getSingleResult();
return mapper.apply(singleResult);
} catch (NoResultException e) {
return null;
}
}
/**
* 查询数量
*
* @param countSql count 语句
* @param paramMap 参数
* @return 数量
*/
protected long nativeCountQuery(String countSql, Map<String, Object> paramMap) {
Query countQuery = entityManager.createNativeQuery(countSql);
// 2 设置参数
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(countQuery::setParameter);
}
Number result = (Number) countQuery.getSingleResult();
return result != null ? result.longValue() : 0L;
}
/**
* 执行原生的更新语句
*
* @param updateSql 语句
* @param paramMap 查询参数
* @return 影响行数
*/
protected int executeNativeUpdate(String updateSql, Map<String, Object> paramMap) {
// 1 创建主查询对象(返回 Tuple)
Query dataQuery = entityManager.createNativeQuery(updateSql);
// 2 设置参数
if (CollUtil.isNotEmpty(paramMap)) {
paramMap.forEach(dataQuery::setParameter);
}
// 3 执行更新
return dataQuery.executeUpdate();
}
}
3 类型转换方法
我这里所有类型转换都放在 公共mapstruct 实现接口内
package com.office.exam.mapper;
import cn.hutool.core.convert.Convert;
import com.office.exam.entity.vo.ExamInfoPageVo;
import jakarta.persistence.Tuple;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 含常见的类型转换
* <p> 1)若修改部分字段,可使用(默认仅覆盖值不为空的字段):void updateEntity(Dto source @MappingTarget Entity target
* <p> 可添加配合方法,实现空值也替换:
* <pre>
* default void updateFields(Dto source, @MappingTarget Entity target) {
* // 覆盖 name 字段,无论 source的 Name 是否为 null
* target.setName(source.getName());
* }
* </pre>
*/
@Mapper
public interface ExamInfoMapper {
ExamInfoMapper INSTANCE = Mappers.getMapper(ExamInfoMapper.class);
default ExamInfoPageVo toPageVo(Tuple tuple) {
if (tuple == null) {
return null;
}
return new ExamInfoPageVo()
.setId(Convert.toLong(tuple.get("id")))
.setExamName(Convert.toStr(tuple.get("exam_name")))
.setPaperName(Convert.toStr(tuple.get("paper_name")))
.setDuration(Convert.toInt(tuple.get("duration")))
.setScore(Convert.toBigDecimal(tuple.get("score")));
}
}
1、为了防止某些列不存在,报错,可用如下工具类获取值:DbSqlUtil
如:DbSqlUtil.tupleGet(tuple, “collection_id”, Long.class)
2、由于自定义排序字段可能来着 org.springframework.data.domain.Pageable,可用:
dataSql += DbSqlUtil.buildSort(pageable.getSort(), " ei.id DESC") 进行安全排序字段拼接
package com.server.common.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.persistence.Tuple;
import jakarta.persistence.TupleElement;
import org.springframework.data.domain.Sort;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.regex.Pattern;
/**
* SQL 安全校验工具类,用于防止 SQL 注入
*/
public final class DbSqlUtil {
// 允许:字母 数字 下划线 空格 英文点
private static final String SAFE_REGEX = "^[a-zA-Z0-9_.\\s]+$";
// SQL关键字(统一小写匹配)
private static final Pattern KEYWORD_PATTERN = Pattern.compile("\\b(select|insert|delete|update|drop|union|or|and)\\b");
// 危险符号
private static final Pattern DANGER_PATTERN = Pattern.compile("(--|;|/\\*|\\*/|\\||\\$|\\{|\\}|\\(|\\)|'\")");
/**
* 校验排序字段值是否安全,防止 SQL 注入
*
* @param value 待校验的值
* @throws IllegalArgumentException 如果值不安全
*/
public static void validateSafeValue(String value) {
if (StrUtil.isBlank(value)) {
return;
}
String v = value.trim().toLowerCase();
// 1 安全字符串检查
if (!ReUtil.isMatch(SAFE_REGEX, v)) {
throw new IllegalArgumentException("非法参数");
}
// 2 关键字检查
if (KEYWORD_PATTERN.matcher(v).find()) {
throw new IllegalArgumentException("非法参数");
}
// 3 危险符号 校验
if (DANGER_PATTERN.matcher(v).find()) {
throw new IllegalArgumentException("非法参数");
}
}
/**
* 校验并返回安全的排序字符串
*
* @param sortString 排序字符串,如 "startTime:DESC" 或 "startTime:DESC, endTime:ASC"
* @param defaultSort 默认排序,如 "RES.START_TIME_ DESC"
* @return 安全的排序字符串
*/
public static String buildSort(String sortString, String defaultSort) {
if (StrUtil.isBlank(sortString)) {
return defaultSort;
}
String[] fields = sortString.split(",");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fields.length; i++) {
if (i > 0) sb.append(", ");
String field = fields[i].replace(":", " ").trim();
validateSafeValue(field);
sb.append(field);
}
return sb.toString();
}
/**
* 校验并返回安全的排序字符串
*
* @param sort Pageable 的 Sort 对象
* @param defaultSort 默认排序
* @return 安全的排序字符串
*/
public static String buildSort(Sort sort, String defaultSort) {
if (sort == null || !sort.isSorted()) {
return defaultSort;
}
return buildSort(sort.toString(), defaultSort);
}
/**
* 安全从 Tuple 中获取值,字段不存在时返回 null,不会抛异常
*
* @param tuple 查询结果
* @param alias 字段别名
* @param type 目标类型
* @return 转换后的值,字段不存在时返回 null
*/
public static <T> T tupleGet(Tuple tuple, String alias, Class<T> type) {
if (tuple == null) {
return null;
}
for (TupleElement<?> element : tuple.getElements()) {
if (alias.equalsIgnoreCase(element.getAlias())) {
Object value = tuple.get(alias);
if (type == LocalDate.class) {
LocalDateTime localDateTime = Convert.toLocalDateTime(value);
return ObjectUtil.isNull(localDateTime) ? null : type.cast(localDateTime.toLocalDate());
}
return Convert.convert(type, value);
}
}
return null;
}
}
4 空分页转换辅助
若查询结果为空分页:CollUtil.isEmpty(page.getContent()),且还需转换成另一个分页类型
可直接用工具类返回:return PageUtil.emptyPage(page)
package com.server.common.util;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import java.util.Collections;
/**
* 分页工具类
*/
public class PageUtil {
/**
* 默认每页大小
*/
private static final int DEFAULT_PAGE_SIZE = 10;
/**
* 空内容分页结果
*/
private static final Page<?> EMPTY_PAGE = new PageImpl<>(Collections.emptyList(), PageRequest.of(0, DEFAULT_PAGE_SIZE), 0);
/**
* 创建空内容分页结果
*
* @param <T> 分页元素类型
* @return 空分页结果
*/
@SuppressWarnings("unchecked")
public static <T> Page<T> emptyPage() {
return (Page<T>) EMPTY_PAGE;
}
/**
* 创建空内容分页结果(指定页码)
*
* @param pageNumber 页码
* @param pageSize 每页大小
* @param <T> 分页元素类型
* @return 空分页结果
*/
public static <T> Page<T> emptyPage(int pageNumber, int pageSize) {
return new PageImpl<>(Collections.emptyList(), PageRequest.of(pageNumber, pageSize), 0);
}
/**
* 根据原始分页创建空内容分页结果
*
* @param originalPage 原始分页
* @param <T> 分页元素类型
* @return 空分页结果
*/
public static <T> Page<T> emptyPage(Page<?> originalPage) {
// 1 若数据 or 分页新为空,则返回空列表
if (ObjectUtil.isNull(originalPage) || ObjectUtil.isNull(originalPage.getPageable())) {
return emptyPage();
}
// 2 不是标准的page 返回空列表
Pageable pageable = originalPage.getPageable();
if (!pageable.isPaged()) {
return emptyPage();
}
return emptyPage(pageable.getPageNumber(), pageable.getPageSize());
}
}
更多推荐


所有评论(0)