说明

使用方法要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());
    }
}

更多推荐