ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Hibernate投影Projections实战:从性能杀手到聚合查询利器

Hibernate投影Projections实战:从性能杀手到聚合查询利器 大家好我是搞了十来年 Java 后端的老兵。今天想聊一个有点“怀旧”味道的技术点——Hibernate 的投影Projections。先回应一下最近常看到的热搜词“hibernate 还有人用吗”。说实话新项目里直接写 Hibernate 原生 Session 的确实少了但你要是翻翻老系统的代码或者看看 Spring Data JPA 底层的实现就会发现 Hibernate 依然稳稳地占据着一大片存量江山。我自己这几年维护的项目里最老的一个从 2008 年跑到现在里面密密麻麻的 Hibernate 查询就是靠 Projections 撑起来的报表统计逻辑。所以这玩意儿不仅没死反而是在大量线上代码里默默干活的“老黄牛”。这篇博文适合谁一是刚接手老项目、对着 Criteria 和 Projections 一脸懵的新人二是知道有 Projections 这回事但只会写session.createQuery(from User)的老手三是做技术选型时想评估 Hibernate 到底还有没有价值的朋友。我会从“投影是什么”讲到“怎么用”再讲“踩坑实录”最后聊聊它在 JPA 时代的延续。全程基于我的真实项目经验代码可以直接抄过去改改就跑。1. Projections 到底是干什么的1.1 从一个浪费的查询说起先看一段绝大多数人刚接触 Hibernate 时都干过的事查一张用户表只想要用户名和手机号结果写的是from User。然后 Hibernate 老老实实把 User 表的所有字段全查出来再塞进内存里你用的时候只取其中两个字段剩下的全成了垃圾。这就像你去菜市场买菜本来只想买两根葱结果把整个摊位都包圆了剩下的菜放坏了还得自己掏垃圾处理费。数据量小的时候无感一旦上了百万级这种全字段查询就是性能和内存的双重杀手。Projections 解决的就是这个问题——它在 SQL 的层面直接让你指定“我要查哪几列”把 Hibernate 的查询结果从“整表实体”缩小成“若干字段”或者“聚合计算后的值”。它对应到 SQL 上就是 SELECT 子句里的字段列表和聚合函数select name, phone from user也好select count(*) from user也好都是 Projections 的活。我记得最早接触这个概念是在写一个后台用户统计报表的时候一张表 40 多个字段我只想要里面 3 个做展示用 Projections 一下就把查询时间从 800ms 降到了 120ms效果立竿见影。1.2 Projections 的本质与适用场景从 Hibernate 的设计来看Criteria 查询本来就是为了解决“在代码里动态拼查询条件”的痛点。而 Projections 是 Criteria 体系中的一个核心组件它在org.hibernate.criterion包里通过一个静态工厂类Projections提供各种投影方法。你可以把它理解成一条 SQL 语句里的“SELECT 部分”的 Java 化表达——Projections.property(name)就是select nameProjections.rowCount()就是select count(*)。适用场景通常有这么几类第一类是列表页展示只需要实体里的几个字段不想把整个大对象拖出来第二类是统计报表需要求和、平均、最大最小、分组数量这些用 Java 内存去算既丑陋又低效第三类是判断记录是否存在只需要 count 一下不需要真的查出实体。还有一类是批量更新或批量操作前的数据验证也经常用投影先查一下目标数据的关键字段避免拉出整个实体。必须提醒一句Projections 是 Hibernate 原生 Criteria API 的产物。你用的如果是 JPA 标准接口那对应的是CriteriaBuilder里的construct、array、count等方法。两者思路一致但 API 长得完全不一样。后面我会单独讲它们之间的对应关系因为很多从 Hibernate 5 往 Hibernate 6 升级的朋友最容易在这里懵掉。2. 核心 API 逐个拆解从 property 到聚合函数2.1 单列投影Projections.property 与别名最基础的投影是查单个字段。假设我有一张用户表 User里面有 name、phone、age、status 等字段我只想查所有用户的姓名列表代码是这样的Criteria criteria session.createCriteria(User.class); criteria.setProjection(Projections.property(name)); ListString names criteria.list();这里有个容易踩的坑criteria.list()返回的 List 元素类型不一定是 String。Hibernate 会根据你投影的字段类型自动推断但如果你投影的是一个关联对象的属性比如Projections.property(department.name)那返回的元素类型就可能是别的 Object你只能拿到 Object 再手动 cast。我建议不管什么情况都养成“收到 List 之后先打印一下元素类型”的习惯别直接强转不然 ClassCastException 在运行时蹦出来的时候排查成本不低。单列投影没太大意思真正有价值的是给投影字段起别名。别名的作用有二一是让返回的 Map 形式结果有清晰的 key二是配合setResultTransformer转换成 DTO。举个具体场景我要查用户的姓名和手机号但我希望结果里能区分哪个是 name 哪个是 phoneCriteria criteria session.createCriteria(User.class); criteria.setProjection(Projections.alias(Projections.property(name), name)); criteria.setProjection(Projections.alias(Projections.property(phone), phone)); // 注意这样写是错误的上面这种写法不对因为第二次调用setProjection会覆盖第一次的设置。正确做法是用ProjectionList组合多个投影我放到 2.3 里专门讲。这里先记住一个核心原则一次查询里要投影多个字段就必须用Projections.projectionList()把它们装在一起单独多次setProjection只有最后一次生效。这个坑我当年踩过调了半天发现只返回了一个字段气得不行。2.2 聚合函数count、rowCount、sum、avg、max、min聚合场景是 Projections 的重头戏尤其是写报表的时候。先看表格投影方法对应 SQL返回类型注意事项Projections.rowCount()count(*)Long统计总行数不会返回 null最安全Projections.count(name)count(name)Long只统计非 null 的行如果全部是 null返回 0Projections.countDistinct(name)count(distinct name)Long去重统计注意字段 null 值被忽略Projections.sum(balance)sum(balance)取决于字段类型如果所有行都是 null聚合结果是 null不是 0Projections.avg(age)avg(age)DoubleHibernate 会转换成 DoubleProjections.max(createTime)max(createTime)与字段类型一致适合取最新时间Projections.min(age)min(age)与字段类型一致适合取最小年龄写一个稍完整的分组统计例子。场景按用户状态统计数量同时计算各状态用户的平均年龄Criteria criteria session.createCriteria(User.class); ProjectionList projectionList Projections.projectionList() .add(Projections.groupProperty(status)) .add(Projections.rowCount()) .add(Projections.avg(age)); criteria.setProjection(projectionList); criteria.addOrder(Order.asc(status)); ListObject[] results criteria.list(); for (Object[] row : results) { String status (String) row[0]; Long count (Long) row[1]; Double avgAge (Double) row[2]; System.out.println(status - count 人, 平均年龄 avgAge); }这里要特别说明几个细节。第一rowCount()返回的是 Long但count(age)当表里没有任何数据时SQL 端返回的是 0但sum(balance)没数据时 SQL 端返回的是 nullHibernate 会把这个 null 原样放进结果对象里。所以你在 Java 代码里做后续计算就得先判空。我在一个财务对账功能里就吃过这个亏直接用Number.longValue()去取 sum 结果空表直接抛空指针排查了半天才发现不是代码逻辑问题而是 SQL 聚合的自然行为。第二groupProperty对应的是 SQL 的group by但它的返回值会作为结果集的一列。如果你分组的字段是个枚举类型那 row[0] 拿到的就是枚举对象本身不是字符串或数字这个和普通 property 投影一致取决于实体字段的 Java 类型。第三avg的返回类型 Hibernate 会统一转成 Double。如果你数据库里存的是 BigDecimalJava 端拿到的却可能是 Double这时候精度就有损失。金额统计千万别用avg配合 Double 去做要改用数据库层处理好精度或者投影原始字段到 Java 再用 BigDecimal 算。2.3 组合投影ProjectionList 与结果取数多字段组合投影的写法是Projections.projectionList()Criteria criteria session.createCriteria(User.class); ProjectionList projectionList Projections.projectionList() .add(Projections.property(name)) .add(Projections.property(phone)) .add(Projections.property(age)); criteria.setProjection(projectionList); ListObject[] list criteria.list();这个时候criteria.list()返回的每个元素是一个Object[]数组的顺序和你 add 的顺序一致。上面这个例子row[0] 是 namerow[1] 是 phonerow[2] 是 age。这种“按位置取数”的方式读起来非常靠记忆一旦后面有人改了投影顺序你前面的取值逻辑就全乱了。更优雅的写法是给每个投影加别名然后结合setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP)把结果转成 MapCriteria criteria session.createCriteria(User.class); ProjectionList projectionList Projections.projectionList() .add(Projections.alias(Projections.property(name), name)) .add(Projections.alias(Projections.property(phone), phone)); criteria.setProjection(projectionList); criteria.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP); ListMapString, Object list criteria.list(); for (MapString, Object row : list) { String name (String) row.get(name); String phone (String) row.get(phone); }需要注意Transformers.ALIAS_TO_ENTITY_MAP返回的 Map 并不是一个普通的 HashMap而是一个大小写不敏感的 Map在后续代码里做row.get(NAME)和row.get(name)效果一样。这个特性日常用起来方便但如果你的代码里依赖 Map 的containsKey做精确判断就可能出现意外。我在一次导出 Excel 的时候想判断 Map 里有没有某个列因为大小写问题导致误判后来改成遍历 key 做精确匹配才消停。还有一点如果你想要更严格的类型映射可以用Transformers.aliasToBean(SomeDTO.class)把结果直接转换成 DTO。但前提是投影别名必须和 DTO 的属性名完全一致。这个我在第 3 大部分再展开。3. 实操过程从报表需求到可运行代码3.1 统计场景最近 30 天订单状态分布我拿一个真实的报表需求来演示 Projections 的完整链路。假设有个订单表 Order字段包括 orderNo、status、amount、createTime。现在要统计最近 30 天里各订单状态的数量和金额总和。SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); Date thirtyDaysAgo ...; // 具体计算略用 Calendar 减 30 天即可 Criteria criteria session.createCriteria(Order.class); criteria.add(Restrictions.ge(createTime, thirtyDaysAgo)); ProjectionList projectionList Projections.projectionList() .add(Projections.groupProperty(status)) .add(Projections.count(orderNo)) .add(Projections.sum(amount)); criteria.setProjection(projectionList); ListObject[] results criteria.list(); for (Object[] row : results) { Integer status (Integer) row[0]; Long orderCount (Long) row[1]; BigDecimal totalAmount (BigDecimal) row[2]; System.out.println(状态 status 订单数 orderCount 总金额 totalAmount); }这段代码里隐藏了一个细节Restrictions.ge(createTime, thirtyDaysAgo)是纯粹的条件过滤不会干扰投影结果。但在同一 Criteria 上既加条件、又加投影、又加排序它们的执行顺序有讲究。Hibernate 会先把投影装进 SQL 的 SELECT 子句再把条件装进 WHERE分组装进 GROUP BY排序装进 ORDER BY。你可以开启hibernate.show_sql看看实际生成的 SQL我每次调投影相关代码都会打开这个开关它对排错价值极大。实际跑数据的时候如果表里有大量历史订单光加一个时间条件还不够。我一般还会配合分页比如只取前 20 行统计数据。这时候有一个经典坑在投影查询里使用criteria.setFirstResult()和criteria.setMaxResults()Hibernate 5 以前生成的 SQL 有时会带上order by到子查询里导致分页结果漂移。后面我在常见问题里细说。3.2 分页排序与投影的组合实践区分“列表展示”和“统计报表”两种场景对排序的需求完全不同。列表展示经常要排序列很多统计报表则通常是按分组字段排序。来看一个组合了投影、排序、分页的代码Criteria criteria session.createCriteria(User.class); criteria.setProjection( Projections.projectionList() .add(Projections.property(name)) .add(Projections.property(age)) ); criteria.addOrder(Order.asc(age)); criteria.setFirstResult(0); criteria.setMaxResults(50); ListObject[] page criteria.list();这样查出来的就是一个按年龄升序排列的部分字段列表。执行这条查询时Hibernate 生成的 SQL 大概是select name, age from user order by age asc limit ?而不是先把整个 user 表查出来再排序再截取。这就是投影配合分页的价值——内存占用小执行效率高。但是有一个细节容易出问题当你给投影加了别名后排序条件必须使用实体属性名不能使用别名。假设你写成criteria.addOrder(Order.asc(ageAlias))Hibernate 可能不会报错而是生成一个无法识别的排序字段或者干脆忽略。这个行为在不同 Hibernate 版本里表现不一致我在 Hibernate 4 的老项目里遇到过直接抛异常在 Hibernate 5 里遇到过正常执行但排序无效。为了稳妥排序一律写实体属性名。分页在统计报表里有一个经典场景统计所有用户分组后的总人数只需要rowCount()和groupProperty(status)但你要的是总的分组数而不是每组的人数。这个需求其实要用到Projections.countDistinct(status)或者rowCount()配合 distinct不要想当然地直接 count。我写过一版错误代码想要分组数量却把每组人数给出来了展示层数字全不对查数据才发现问题出在投影语义理解上。3.3 投影结果转 DTO 的两种姿势开发中很少真的拿一个Object[]直接去渲染页面通常要转成有语义的 VO/DTO。这里有两种姿势我先对比一下。第一种是手动行转 DTO。优点是完全可控缺点是对字段多的时候重复代码多。适合字段少、结构稳定的场景ListObject[] rows criteria.list(); ListUserBriefDTO dtoList new ArrayList(); for (Object[] row : rows) { UserBriefDTO dto new UserBriefDTO(); dto.setName((String) row[0]); dto.setPhone((String) row[1]); dtoList.add(dto); }第二种是借助Transformers.aliasToBean。前提有两个投影必须加别名DTO 必须有对应属性名的 setter 方法Criteria criteria session.createCriteria(User.class); ProjectionList projectionList Projections.projectionList() .add(Projections.alias(Projections.property(name), name)) .add(Projections.alias(Projections.property(phone), phone)) .add(Projections.alias(Projections.property(age), age)); criteria.setProjection(projectionList); criteria.setResultTransformer(Transformers.aliasToBean(UserBriefDTO.class)); ListUserBriefDTO list criteria.list();这里面有几个很阴的坑。第一aliasToBean使用的是 Introspector 机制它要求 DTO 的属性有标准的 setter而且属性名与别名大小写要保持一致。第二如果你的 DTO 属性是String但数据库/实体的对应字段是IntegerHibernate 在转换时不会自动帮你转而是直接抛异常。第三DTO 一定要有无参构造器否则反射就直接失败。我在一次导出用户列表的功能里用了aliasToBean结果因为 DTO 里一个phone字段类型写成了Integer查询好好的转换崩了。报错信息又长又绕最后是打开 debug 日志看到org.hibernate.property.access.spi.SetterMethodImpl里的类型不匹配才发现根因。从那以后我一旦用aliasToBean就会在写完 DTO 后先跑一个空数据集测试确保反射问题尽早暴露。不过说句实话老项目里旧 Criteria API 配合aliasToBean是真方便但在维护性上这种“隐式约定”确实比不过显式映射。如果团队里新人多我反而推荐手动写一行行 setter虽然啰嗦但出问题一眼能看出来。4. 常见问题与排查技巧实录4.1 ClassCastException 和类型不匹配投影查询最容易出现的错误是类型转换异常原因有三个。第一个是数据库字段类型和 Java 类型本身就不同。比如 MySQL 的datetime映射到 Hibernate 实体里是Timestamp你投影createTime时以为拿到的会是Date运行时直接强转(Date) row[0]就会炸。这个解决思路是投影时明确用Projections.cast做类型转换或者在 DTO 里声明成匹配的类型。第二个是聚合函数的隐式转换。前面提过avg返回 Doublesum对 BigDecimal 字段返回 BigDecimal但如果你字段是 Integersum返回的可能就是 Long 或 Integer不同数据库方言下行为还会漂移。我的经验是只要涉及聚合函数就在代码里用Number作为接收类型而不是写死Long或Double。比如Number sumValue (Number) row[2]; BigDecimal total BigDecimal.valueOf(sumValue.doubleValue());虽然多了一步但至少不会因为类型漂移直接崩。第三个是投影 NULL 值。SQL 里聚合函数作用于空表会返回一行且值为 NULL但Object[]数组里这个位置会是 null。你在循环里直接拆箱就空指针。建议所有投影取数的地方统一写一个取值工具方法类似Objects.toString(row[0], )或者BigDecimal.valueOf(((Number) row[0]).doubleValue())但先判空。这种工具方法我每个项目里都会保留一份是真的能省不少事。4.2 distinct 与 count 的经典混淆先看这段代码criteria.setProjection( Projections.projectionList() .add(Projections.countDistinct(status)) .add(Projections.groupProperty(status)) );你以为你是在“按状态分组并统计每组数量”实际上这个组合非常奇怪。countDistinct(status)的意思是统计该列去重后的非空值数量而你同时又在按 status 分组分组后 status 本身就是每个组一个值distinct 对组内已经没有意义。更典型的问题是如果你只写Projections.count(name)而不写 groupProperty那 SQL 会退化成一行全局统计和你脑海里“每个名字出现次数”完全对不上。所以用 count 系列之前先自问一句我到底是想统计全局行数、非空字段数、还是分组后的组内数量。这三种分别对应全局行数rowCount()或count(*)不需要 groupBy。非空字段数count(name)不需要 groupBy统计全表 name 非空的记录数。分组后的组内数先groupProperty(status)再rowCount()或count(id)这里 count 里的字段无所谓只要非空即可。用countDistinct的场景通常是去重统计比如统计有多少用户下过订单如果你用rowCount()统计订单表会重复计算同一个用户的多个订单这时countDistinct(userId)才是正解。4.3 别名失效与 Hibernate 6 的调整升级到 Hibernate 6 之后最让人头疼的是老一批 Criteria API 被标记为 deprecated 甚至直接移除。org.hibernate.Criteria在 Hibernate 6 里已经不再是推荐方式但org.hibernate.criterion.Projections还在只是内部实现已经换了一套。如果你在老项目里使用 Hibernate 6然后代码还保留了Projections.alias(Projections.property(x), x)的写法它在某些关联查询场景下 alias 可能不再传递到 SQL 别名里导致aliasToBean拿不到正确的列名。我遇到过一个小项目从 Hibernate 5.6 升到 6.1原本跑得好好的投影查询升级后有几条 SQL 生成的列名变成了y0_这种别名aliasToBean直接匹配不上。这种问题排查起来很费劲因为单条 SQL 单独看是对的只是别名和 DTO 约定的 name 对不上。我当时的解决办法是在 Hibernate 6 下放弃旧Criteria改用 JPA 的CriteriaBuilder重写那几条关键查询。这也是我后面推荐的方向——老项目能跑就别乱升真要升投影查询得优先改造。4.4 分页漂移和 show_sql 的排查价值投影加排序再加分页时有个隐蔽问题如果你在 SQL 里用了distinct投影然后又setMaxResultsHibernate 5 以前的旧版可能生成错误的 SQL导致返回记录数不对或顺序错乱。报错不一定明显但数据对不上。排查这类问题我最先做的一定是打开hibernate.show_sqltrue。这一步能直接看到 SQL 长什么样是不是真带上了limit是不是把order by放进了子查询是不是别名莫名其妙。开启方式hibernate.show_sqltrue hibernate.format_sqltrue在 Spring Boot 的 application.properties 里同时开这两项然后去控制台看实际 SQL。我一次项目上线前跑批任务投影groupBy分页的组合查出来的记录总数和 SQL 直接查不一致开 show_sql 才发现 Hibernate 生成的 SQL 把 group by 的字段漏掉了因为它默认把投影的 groupProperty 和 select 字段合并逻辑弄复杂了。最后靠手动改成createSQLQuery才绕过去。能把问题定位到 SQL 层就已经解决了一半。5. Hibernate 6 与 JPA Criteria 中投影的对应5.1 老 Criteria 被废弃之后怎么办Hibernate 6 里org.hibernate.Criteria老接口已经不推荐使用了取而代之的是 JPA 标准的CriteriaQuery和CriteriaBuilder。很多老项目的代码里session.createCriteria(Entity.class)这行直接编译报错得改写成CriteriaBuilder cb session.getCriteriaBuilder(); CriteriaQueryTuple query cb.createTupleQuery(); RootUser root query.from(User.class); query.multiselect(root.get(name), root.get(phone));这看起来麻烦但本质上做的事情和 Projections 一样只是把“列清单”从ProjectionList换成了multiselect。如果你之前用Object[]接结果JPA 里也有对应的Object[]查询就是把泛型换成CriteriaQueryObject[]然后query.select(cb.array(root.get(name), root.get(phone)))。我在迁移一个老模块的投影查询时踩过一个坑JPA 的multiselect默认返回类型不是Tuple就是Object[]但如果你在multiselect里只放了一个字段返回类型就退化成那个字段本身的类型。这会导致你的代码对返回元素的处理出现二义性。所以迁移时我建议统一用createTupleQuery()然后用tuple.get(alias)取数。5.2 新旧 API 的对照速查表如果你是从旧 Projections 迁移到 JPA Criteria下面这张对照表可以直接收藏场景旧 Hibernate CriteriaJPA CriteriaBuilder单列投影Projections.property(name)cb.select(root.get(name))或multiselect查所有列不设投影query.select(root)行数统计Projections.rowCount()cb.count(root)或cb.countDistinct(root)非空计数Projections.count(name)cb.count(root.get(name))去重计数Projections.countDistinct(name)cb.countDistinct(root.get(name))求和Projections.sum(amount)cb.sum(root.get(amount))平均Projections.avg(age)cb.avg(root.get(age))最大/最小Projections.max(createTime)cb.max(root.get(createTime))/cb.min(...)分组Projections.groupProperty(status)query.groupBy(root.get(status))别名Projections.alias(...)root.get(name).alias(name)注意旧代码里Projections.groupProperty(status)是“投影分组”二合一JPA 里这两件事是拆开的query.groupBy(root.get(status))负责分组query.multiselect(root.get(status), cb.count(...))负责投影。逻辑更清晰但也更容易漏写。我迁移时最容易漏掉的就是groupBy导致生成了没有 group by 但 select 里有分组字段的 SQL数据库直接报错。5.3 在 Spring Data JPA 的 Specification 里写投影现在很多新项目不会直接拿 Session 写 Criteria而是用 Spring Data JPA 的Specification来做动态查询。Specification底层用的其实也是 JPA Criteria所以你可以在 Specification 里构造出带投影的查询public class UserNameSpecification implements SpecificationUser { Override public Predicate toPredicate(RootUser root, CriteriaQuery? query, CriteriaBuilder cb) { query.multiselect(root.get(name), root.get(phone)); return cb.equal(root.get(status), 1); } }但这里有个大坑Spring Data JPA 的findAll(Specification)方法底层会强制把CriteriaQuery的返回类型设置成实体类型。你在 Specification 里调multiselect运行时它不一定能按你的投影来因为 repository 的泛型方法已经定死了返回类型。所以专门做投影查询时我一般不会硬塞进findAll(Specification)里而是自己注入EntityManager手动走 JPA Criteria 或者旧 Criteria。这点团队里经常有人问我我统一解释是框架替你包了一层同时把灵活性也包没了。如果你真的想在新项目里用投影最简单的方案反而是直接写 JPQLTypedQueryObject[] query em.createQuery(select u.name, u.phone from User u where u.status :status, Object[].class); ListObject[] list query.setParameter(status, 1).getResultList();这段代码虽然短但有些老团队的编码规范会禁止裸 SQL 风格的字符串语句。所以我一般建议能用 Specification 就用 Specification默认非投影确实要投影看清返回类型再决定是走EntityManager还是 JPQL别稀里糊涂两边混着写。6. 我个人踩过的一些坑希望大家绕开Projections 最大的价值是让你“少查字段、多算聚合”但它也有一个容易被忽略的小缺点它破坏了实体查询的懒加载机制。你想想假如你投影了订单表的 userId然后业务逻辑想通过这个 userId 去拿用户对象你取到的只是一个 Long还得再发一次查询。如果你当初直接查 Order 实体配合 ManyToOne 的懒加载Hibernate 会在会话内帮你搞定或代理。所以投影不是万能的它只适合展示独立字段或统计聚合的场景不适合要回实体关联的场景。另一个经验是凡是投影返回Object[]的地方都要在代码里写注释说明“数组每个下标对应哪个字段”。我见过一个项目里 10 个地方用投影结果有 3 处顺序错了因为早先有人在前面的投影列表里插入了一个新字段后面的 row 下标全部错位。用注释把下标和字段的对应关系写清楚能省掉后面接手的人大量查代码的时间。更好一点的做法是彻底改成 Map 或者 DTO 返回虽然慢一丁点但可读性提升一个档次。最后说回“hibernate 还有人用吗”这个热搜。我的看法很简单技术栈没有真正意义上的“死”只有持续迁移中的“半衰”。Hibernate 的投影思想——在数据库层面只取需要的列、把聚合计算压到 SQL 层——在任何 ORM 里都成立。你学会了 Projections再去看 JPA 的 CriteriaBuilder 或者 QueryDSL会发现都是同一个套路。咱们这行真正保值的是底层的思路而不是表面那层 API。老代码里的 Hibernate 还在跑新项目里的 JPA 底层还是 Hibernate所以把投影这套搞明白不管对存量系统还是新系统都有实实在在的收益。
返回列表