Spring Data JPA复杂查询实战:用JPQL手写SQL替代方法名查询
做后端快三年的朋友可能都有这种体会Spring Data JPA 用久了最顺手的是那些findByUsernameAndStatus之类的方法名查询一眼就能看懂根本不用写 SQL。可一旦遇到多表关联、聚合统计、动态条件、批量更新那串方法名就会变得又臭又长甚至直接编译报错。这个系列第 8 篇我就专门讲怎么在 Spring Boot 2.0.3 MySQL 5.7 这套组合下用 JPQL 手写 SQL把这层窗户纸捅破。1. 什么时候该抛弃方法名查询1.1 方法名查询的舒适区与天花板先说说方法名查询有多舒服。写一个UserRepository只要继承JpaRepository什么都不干框架就会根据方法名自动生成实现。比如public interface UserRepository extends JpaRepositoryUser, Long { ListUser findByUsernameAndStatus(String username, Integer status); ListUser findByAgeGreaterThanOrderByCreateTimeDesc(Integer age); }这种写法在条件固定、字段较少、单表查询的场景下非常高效也是我最开始接触 JPA 时最喜欢的部分。但舒适区总有边界一件事情我印象特别深某个模块需要统计不同订单状态下的销售额总和还要按商品分类分组。当时的OrderRepository方法名几乎没法表达这种需求硬凑出来是这样的// 这种写法一旦字段一多连自己都看不下去 ListObject[] findSomethingBy... // 根本不知道怎么往下拼接最后还是老老实实回到了写查询语句的老路。1.2 为什么选择 JPQL 而不是原生 SQL在实际项目里很多人一遇到复杂查询第一反应是直接用Query(nativeQuery true)写原生 SQL。这确实能解决很多问题但有一个长期隐患SQL 与数据库方言深度耦合。比如LIMIT语法在 MySQL 下能跑换成 Oracle 或 PostgreSQL 就可能出问题。而 JPQL 操作的是实体对象和属性底层会根据配置的方言自动翻译成对应的 SQL从跨数据库的角度看是更稳的选择。更重要的是JPQL 能直接和实体状态联动。比如多表关联时JPQL 可以直接写order.customer.username导航到Order实体关联的Customer属性而原生 SQL 通常要自己做 join 和结果映射。这个开发效率差异在字段多的场景下非常明显。所以我的建议是单表固定查询用方法名跨库兼容性要求高或者实体关联关系复杂时优先 JPQL只有涉及索引优化、复杂报表或 MySQL 特有语法如FOR UPDATE这类时才用原生 SQL。2. JPQL 不是 SQL先搞懂这套思维转换2.1 查的是实体不是表JPQL 和 SQL 最大的区别就是操作对象不同。看一个最简单例子Query(select u from User u where u.age ?1) ListUser findAdultUsers(Integer age);这里的User是实体类名u.age对应的是User类中的age属性而不是数据库表里的age列。这个转换是 JPA 通过Table、Column等注解完成的。所以写 JPQL 之前脑子里一定要有一个实体模型图而不是 ER 图。这个转换逻辑也解释了为什么调select u时不需要写select *JPQL 的select u默认会把整个实体所有字段都查出来等同于 SQL 的select *。如果只想要部分字段就要显式列出属性比如select u.username, u.age from User u。2.2 别名为什么这么讲究JPQL 里的别名不只是为了缩写它是定位实体的关键。select u from User u中u代表了当前查询范围内的User实体的一个引用。多表关联时每个实体都要有自己的别名后续过滤条件、排序、分组都需要通过别名引用属性。这里我踩过的一个坑是别名和表名混淆。数据库里表名通常是下划线风格user_info实体名是帕斯卡风格UserInfoJPQL 里只能写实体名和别名不能写表名。刚开始写 JPQL 时我总是不自觉地写select * from user_info结果立刻报StatePath相关的异常因为 JPA 根本没有去解析数据库表名。2.3 关键字的写法约定JPQL 关键字本身不区分大小写比如SELECT、select都能用但表名、属性名必须严格一致。我个人的习惯是关键字全大写这样和属性名区分明显后期排查问题也快。另外需要注意JPQL 里不能使用*号select *是 SQL 写法在 JPQL 中必须声明别名再查询。3. Query 注解的核心玩法完整拆解3.1 一个最小的 Query 示例Query是手写 JPQL 的入口直接写在 Repository 接口的方法上public interface UserRepository extends JpaRepositoryUser, Long { Query(select u from User u where u.email ?1) OptionalUser findByEmail(String email); Query(select u from User u where u.username :username and u.status :status) ListUser findByUsernameAndStatus(Param(username) String username, Param(status) Integer status); }第一个方法用的是位置参数?1第二个方法用的是命名参数:username。位置参数的问题是一旦方法参数多了很容易传错位置命名参数则必须在每个参数上都加Param注解但可读性明显好很多。我现在的项目里统一用命名参数阅读和维护的成本更低。3.2 参数传递的四种方式除了普通参数实际开发里还会碰到集合、对象、分页框架这几种特殊参数。具体如下表场景写法示例使用要点单个基本类型:status最常用加Param注解集合参数:usernamesJPQL 中配合in (:usernames)使用实体对象:user可以直接用:user.username形式导航属性分页参数Pageable方法参数里加PageableJPQL 不需要写limit集合参数配合IN操作能解决很多批量化查询的需求。具体用法Query(select u from User u where u.username in :usernames) ListUser findByUsernames(Param(usernames) CollectionString usernames);这里有个容易踩的坑in后面必须带括号写成in (:usernames)在某些 JPA 版本下不会自动展开成多值反而会报语法错误。我在 Spring Boot 2.0.3 下实测推荐直接写成in :usernames框架会自动展开为in (?, ?, ?)。3.3 查询结果映射的三种形态JPQL 查询结果的映射方式决定了你后续代码的写法第一种直接返回实体。这是最简单的形态适合单表完整查询。第二种返回Object[]或ListObject[]。聚合查询、部分字段查询时常用每个数组元素就是一行结果。第三种DTO 投影。JPQL 支持直接select new com.example.UserDto(u.id, u.username) from User u但 DTO 必须有对应的构造方法包路径也要写全。实际开发中我一般会在 DTO 投影和实体返回之间做取舍。如果查询结果需要被后续逻辑修改并保存到数据库就返回实体如果只是读出来展示建议直接投影成 DTO避免多余字段传输。3.4 更新和删除必须记住的两个注解JPA 默认查询是读操作执行更新、删除时必须额外加Modifying不然会直接报QueryExecutionRequestException之类的错误。同时这个方法还必须在事务中执行否则更新不会生效。Modifying Transactional Query(update User u set u.status :status where u.id :id) int updateStatus(Param(id) Long id, Param(status) Integer status);注意这里的返回值是int表示受影响的行数。还有个细节是Modifying模式下默认不会触发 JPA 的一级缓存清理如果你后续还在同一事务里查询这个实体需要自己调EntityManager.clear()不然会读到旧数据。这个问题刚接触时很容易忽略。4. 复杂业务场景下的 JPQL 实操案例4.1 多表关联查询怎么做项目里最常见的还是关联查询。比如查询所有订单以及对应的客户名字Query(select o, c.username from Order o left join o.customer c where o.status :status) ListObject[] findOrdersWithCustomer(Param(status) Integer status);这里的关键是left join o.customer c。注意 JPQL 里 join 的不再是表而是实体之间的关联属性。Order 实体里必须有一个customer属性并且配置了ManyToOne或类似的关系映射。很多人第一次写会写成left join Customer c这样也能跑但实际上查的是笛卡尔积而不是关联关系结果往往令人意外。返回结果为什么是Object[]因为select o, c.username查了两个来源JPA 没法自动封装成某个实体所以每行结果是一个数组索引 0 是 Order 对象索引 1 是客户名字字符串。4.2 动态条件该用什么方案JPQL 本身不支持动态拼接条件Query里的字符串默认是固定的。如果查询条件多、组合不确定我知道的靠谱方案有三个方案一用SpecificationJPA Criteria API。官方支持的动态查询方式类型安全但我个人觉得写起来代码量大。方案二用Query配合COALESCE在 SQL 层面对空值做处理。方案三在 Repository 里用default方法拼接 JPQL。我工作中用的是第三个方案看起来最灵活default ListUser searchUsers(String username, Integer status, String email) { StringBuilder jpql new StringBuilder(select u from User u where 1 1); MapString, Object params new HashMap(); if (StringUtils.hasText(username)) { jpql.append( and u.username like :username); params.put(username, % username %); } if (status ! null) { jpql.append( and u.status :status); params.put(status, status); } if (StringUtils.hasText(email)) { jpql.append( and u.email :email); params.put(email, email); } TypedQueryUser query em.createQuery(jpql.toString(), User.class); params.forEach(query::setParameter); return query.getResultList(); }这个方案需要注入EntityManager但是非常直观也没有额外引入任何依赖。4.3 分页查询不要自己拼 SQLJPQL 分页很简单只要方法参数里加上Pageable框架会自动生成limit和offsetQuery(select u from User u order by u.createTime desc) PageUser findAllUsers(Pageable pageable);调用方传入PageRequest.of(0, 10, Sort.by(createTime).descending())就行。这里有一点需要特别注意如果 JPQL 带了order by又在外面套一层PageRequest的排序两边的排序可能会冲突。我的习惯是排序只保留一处避免出现意外的乱序。4.4 聚合统计与分组查询的结果处理聚合查询是不能返回实体的。比如统计每个状态下的用户数量Query(select u.status, count(u) from User u group by u.status) ListObject[] countUsersByStatus();返回的Object[]里索引 0 是分组字段值索引 1 是统计结果Long类型。如果想让调用方用起来更舒服可以做一个 DTO 投影Query(select new com.example.UserStatusCount(u.status, count(u)) from User u group by u.status) ListUserStatusCount countUsersByStatus();DTO 里提供一个接收这两个参数的构造方法即可。其实从这点可以看得出来JPQL 对嵌套函数、分组、聚合的支持并不比原生 SQL 弱多少只是返回结果形态需要自己掌控。5. 手写 JPQL 三个月后我才想明白的坑与经验5.1 列名不存在和属性名写错是两码事JPQL 报错信息里最经典的一条是Unable to locate Attribute with the given name看到这个基本可以断定是属性名写错了。比如实体属性是userName你写成了username就会触发。这个和数据库字段完全无关纯粹是 Java 属性名的匹配问题排查的时候一定要先打开实体类确认属性名的大小写和拼写。5.2 模糊查询的 LIKE 写法不能照搬 SQLJPQL 里like后面拼%的方式和 SQL 不太一样。像where u.username like %张%这种直接写在字符串里是不行的要这样写Query(select u from User u where u.username like concat(%, :keyword, %)) ListUser searchByKeyword(Param(keyword) String keyword);这个方法我在本地 MySQL 5.7 下测试了很多次concat是通用的 JPQL 函数在跨数据库时也比较稳。第一次用like :keyword %的时候直接报语法错误后来才知道不能用来做字符串拼接。5.3 更新操作背后的事务陷阱前面说了更新删除要加Modifying和Transactional但还有一个更隐蔽的问题批量更新和一级缓存。JPA 默认开启了持久化上下文缓存如果先查出来一个实体然后又执行批量更新更新结果不会自动同步到缓存中那个实体对象上。这会导致接下来的读请求拿到旧数据。我的解决办法是在批量更新方法内部处理完之后调用em.flush()和em.clear()强制清缓存。等后续再查询时就会从数据库重新加载保证读到的是最新值。5.4 关联查询 N1 问题的简易规避JPQL 的 join 查询在开发初期很容易触发 N1 查询问题。比如查订单列表时如果没有把关联的客户信息一次性提取出来遍历订单读取order.customer的时候每条记录都会额外发一条 SQL 去查客户表。解决方案是用join fetchQuery(select o from Order o join fetch o.customer c where o.status :status) ListOrder findOrdersWithCustomer(Param(status) Integer status);join fetch会提前把关联对象一并查出避免逐条查询。但要注意join fetch和分页同时使用时某些方言下会报in memory分页的警告具体还取决于数据库方言和关联集合的类型。一对多集合关联时尤其需要留意。5.5 从 Spring Boot 2.0.3 的角度多说一句这一篇是基于 springboot 2.0.3 写的对应的是 Spring Data JPA 2.0.x 的 API 风格。如果你当前项目用的是 Spring Boot 3.x命名参数、Query注解这些核心用法完全没变但 Jakarta 持久化的包名从javax.persistence变成了jakarta.persistence碰到导入报错别慌把包名改过来就好。核心思维还是通用的。回过头来总结一下手写 JPQL 真正的门槛不在语法而在从表思维切换到实体思维。一开始写慢、报错多、心里骂 JPA 是常态等慢慢适应了实体属性导航这种模式之后你会发现复杂查询的代码量反而比以前写原生 SQL 更精简。我个人现在写新功能时默认第一版就是用 JPQL 起手跑不通或者性能瓶颈明显时才会考虑换原生 SQL 做针对性优化。

相关新闻

ArcEngine要素查询实战:属性查询与空间查询联合过滤

ArcEngine要素查询实战:属性查询与空间查询联合过滤

简介:面向ArcEngine入门开发者与GIS二次开发人员的示例工程,演示如何对要素图层执行属性查询与空间查询,涵盖按属性条件筛选记录、按几何关系选取要素等常见交互场景。压缩包共121个文件,以C#源码(.cs)、Fi…

2026/10/13 13:50:36 阅读更多 →
vue-element-admin实战解析:权限路由、动态菜单与后台管理系统脚手架搭建

vue-element-admin实战解析:权限路由、动态菜单与后台管理系统脚手架搭建

简介:这是一套基于 Vue 2 与 Element UI 的企业级后台管理界面模板,适合前端开发者、全栈工程师以及需要快速搭建后台系统的团队。框架集成了动态路由、Vuex 状态管理、基于角色的权限控制、Axios 请求统一封装、国际化多语言、Mock 模拟数据、操作日志记…

2026/10/13 13:50:36 阅读更多 →
用litellm搭建LLM统一网关:路由、密钥与成本控制实战

用litellm搭建LLM统一网关:路由、密钥与成本控制实战

先交代一下背景:我们团队维护的AI应用已经接入了五六个不同厂商的大模型,从闭源旗舰到开源自主部署都有。一开始还能靠每个厂商的SDK挨个对接,但等业务开始并发增长,要处理不同格式的请求体、不同的鉴权方式、不同的超时行为&…

2026/10/13 13:49:34 阅读更多 →

最新新闻

MATLAB生产级MFCC实现:从预加重到Delta-Delta的全流程工程规范

MATLAB生产级MFCC实现:从预加重到Delta-Delta的全流程工程规范

简介:本资源是一份完整的梅尔倒谱系数(MFCC)MATLAB实现代码包,面向语音信号处理初学者、高校课程实践者及语音识别入门开发者,解决语音特征提取这一核心预处理环节的实操需求。包内共23个文件,涵盖11个核心…

2026/10/13 15:29:11 阅读更多 →
C++课设实战:美发店管理系统源码拆解与避坑指南

C++课设实战:美发店管理系统源码拆解与避坑指南

简介:这份资源是面向高校计算机专业学生的C课程设计完整方案,主题为美发店管理系统,适合正在准备期末课程设计或希望用真实项目巩固面向对象编程的学习者。系统涵盖客户管理、预约管理、服务项目、员工管理、库存管理与报表统计六大模块&…

2026/10/13 15:29:11 阅读更多 →
志愿者公益网站毕设开题答辩全攻略:从选题到应答

志愿者公益网站毕设开题答辩全攻略:从选题到应答

站在答辩教室门口的时候,我手里捏着的PPT标题就是《志愿者公益网站的设计与实现》。说实话,这个题目乍看挺"普通"的——网站、设计与实现,每年答辩十个里能撞三四个。但正是这种"普通题",才最容易在开题阶段被…

2026/10/13 15:29:11 阅读更多 →
ThinkPHP Word导出实战:OpenXML规范与流式响应方案

ThinkPHP Word导出实战:OpenXML规范与流式响应方案

简介:本资源是一份面向PHP中级开发者与ThinkPHP框架实践者的实用工具包,聚焦企业级应用中常见的报表生成与文档导出需求,解决动态生成格式化Word文档并一键下载的核心痛点。压缩包为RAR格式,共含2个PHP文件,总大小仅4K…

2026/10/13 15:29:11 阅读更多 →
CodingBat Java调试实战:边界条件、JVM执行与字节码级排错

CodingBat Java调试实战:边界条件、JVM执行与字节码级排错

简介:本资源是面向Java初学者与进阶学习者的CodingBat在线编程平台习题完整解答集,聚焦Java核心语法、面向对象编程、集合框架、字符串处理、递归与异常处理等10大知识点,通过可运行、可测试的代码方案帮助读者夯实基础、验证理解、培养TDD实…

2026/10/13 15:29:11 阅读更多 →
JxBrowser 6.16 破解版:离线嵌入 Chromium 的 Java 浏览器控件方案

JxBrowser 6.16 破解版:离线嵌入 Chromium 的 Java 浏览器控件方案

简介:本资源为JxBrowser 6.16全功能破解版集成包,面向Java桌面应用开发者及Swing/JavaFX嵌入式浏览器需求者,解决商业授权门槛高、本地集成复杂、依赖冲突频发等实际开发痛点。压缩包共1329个文件,主体为1186个已编译class字节码与…

2026/10/13 15:28:08 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →