ARTICLE DETAIL

资讯详情

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

Hibernate别名机制详解:HQL、Criteria与原生SQL的映射规则

Hibernate别名机制详解:HQL、Criteria与原生SQL的映射规则 有人问“hibernate还有人用吗”这个问题我几乎每年都会在技术群里看到一次。我的回答一直很直接只要你的Spring Boot项目还在用Spring Data JPA那底层跑的就是Hibernate。所谓“Hibernate过气”无非是大家不用再天天面对SessionFactory和HQL的细节了框架本身反而活得好好的。也正因如此很多开发者在写JPA的时候遇到一个看似简单的问题反而卡住了Hibernate里的别名Alias到底是什么这个问题的答案其实贯穿了HQL、JPQL、Criteria查询和原生SQL映射的方方面面。如果你总是不清楚select u.name from User u里的这个u是干嘛的或者原生SQL把列映射回实体的规则是什么那么这篇内容就是冲着你来的。我会从别名要解决什么问题讲起再分场景拆解HQL、Criteria、原生SQL里别名的用法和坑最后给一个可以直接收藏的踩坑速查表。1. 别名到底解决什么问题一张关系表上的三个角色1.1 一句话说清别名就是一个引用标签先说最朴素的定义。在SQL里别名就是给表或列起的一个临时名字作用是在一条语句里指代这个对象省得每次都写完整的表名和列名。select u.name from t_user u这里的u就是t_user这张表的别名。到了Hibernate这里含义又扩展了一点。HQL里from User uu是实体别名原生SQL里select u.id as user_iduser_id是列别名Criteria里root.join(pet, JoinType.LEFT)的返回值本质上也是Hibernate帮你管理的路径引用。这三个东西都叫别名但作用位置不同混在一起想就乱了。我用一个生活类比帮新手理解别名相当于微信里的备注名。你不必每次都说“那个头像很酷、上个月一起吃过饭、做后端开发的朋友”只需要说“老张”对方就知道是谁。在关系数据库的查询里也一样一张表一旦出现在多条子句里你不能每次都重复全名得给它一个“老张”式的称呼这就是别名。1.2 三种别名别搞混实体别名、列别名、路径别名把三种别名放在一起对比一次后面就不会混淆了。实体别名作用于HQL/JPQL代表的是持久化实体对象本身列别名作用于SQL结果集代表查询出的某个字段也是原生SQL映射回实体属性的关键路径别名则是Criteria查询里对关联对象引用的一种封装。有个印象特别深的例子。同事在排一个查询问题看到日志里Hibernate生成的SQL是select u1_0.id, p1_0.name from t_user u1_0 left join t_pet p1_0 on u1_0.idp1_0.user_id他问我“这个u1_0是什么我没写过这个别名”。这就是Hibernate在把HQL解析成SQL时自动生成的实体别名规则是实体首字母加数字序号用来保证一条复杂SQL里同名实体不打架。这个概念理解清楚了以后看Hibernate打印的SQL日志就不会觉得是一堆乱码了。2. HQL中的别名查询逻辑的骨架2.1 from子句的实体别名与where引用HQL写出来的第一步就是决定给核心实体一个什么别名。from User u是整个查询的主语后续所有对User字段的限制都通过u.xxx来写。比如要查年龄大于18的用户就写where u.age 18按名字排序就写order by u.name。这里有一个常见误区很多人以为HQL里的别名是给数据库表用的所以误写成了from t_user u。HQL的对象不是表而是实体所以必须是from User u这里的User是Java类名。如果你用原生的t_user写HQLHibernate直接抛org.hibernate.hql.internal.ast.QuerySyntaxException。实体的别名随意起但建议短且有辨识度u、p、o这类就够用不需要起一长串。还有一点值得注意HQL里别名可以不出现在select里只出现在where里。from User u where u.age 20select其实是整行实体这里别名u只负责限定条件。这时候如果打印SQL日志你会看到Hibernate生成的SQL里select了所有字段这是正常的因为Hibernate需要把整个实体组装出来。2.2 隐式路径表达式Hibernate帮你起名有时候你不需要显式声明别名也能写出带关联字段的HQL。比如查所有有宠物的用户from User u where u.pet is not null。这里的u.pet是路径表达式Hibernate会在内部创建一个关于pet关联的隐式别名并自动生成一个inner join的SQL。这种写法省事但有两个风险。一是在一个较长的HQL里如果多次通过路径表达式访问同一个关联的属性Hibernate内部可能会生成多个隐式join虽然语义上等价但生成的SQL可能会有重复join的情况性能和可读性都受影响。二是在select子句里想同时返回User和Pet却不显式声明别名写法会变得非常绕。所以我在实际项目里有一条约定只要一条HQL里出现了两个或以上的实体就明确给每个实体起别名并且把join关系写清楚不要依赖隐式路径。比如from User u left join u.pets p where p.name like %球%。这样代码的可读性、可维护性都强得多排查问题时也能一眼看出是哪张表和哪张表在join。2.3 select子句投影和聚合上下文别名在select子句里还有一个重要任务承载投影结果。select u.name from User u返回的是字符串列表select u from User u返回的是User实体列表。别名放的位置不同结果类型完全不同。更常见的是DTO投影写法这是Hibernate用户必须掌握的select new com.example.UserDTO(u.id, u.name, u.age) from User u where u.age 18这里UserDTO是一个普通Java类必须有对应的构造函数。Hibernate会按位置把别名列出的字段值填入构造函数。这就是我们通常说的“select new”动态实例化。它比返回Object[]再手动转DTO要优雅得多。聚合场景下别名的角色更微妙。select u.age, count(u) from User u group by u.age having count(u) 5这里count(u)中的u不是值本身而是代表“整行实体的数量”。初学者容易把u当成某个字段来理解其实它指向的是查询的主体实体。还有一个官方文档里明确写的规则select子句出现的别名、属性表达式必须在group by或聚合函数中保持一致否则数据库报错只是时间问题。比如按u.age分组同时select了u.name大多数关系型数据库会直接拒绝执行。3. Criteria和API层里的Alias连接与命名的艺术3.1 用createAlias关联查询路径Hibernate的旧版Criteria API里有一个专门为别名设计的方法createAlias(String associationPath, String alias)。很多老项目里的代码是这样写的Criteria criteria session.createCriteria(User.class); criteria.createAlias(pets, p); criteria.add(Restrictions.eq(p.name, 旺财)); ListUser users criteria.list();这里pets是User实体里的关联属性名p是赋给这个关联路径的别名。创建后后续所有关于Pet的条件都通过p.xxx来写不再需要写pets.name这种长路径。这样写的好处是一方面省字另一方面当你需要多次操作同一个关联对象时别名能保证Hibernate只生成一个join。有人会问能不能直接写criteria.add(Restrictions.eq(pets.name, 旺财))能功能上没问题但一旦你需要在同一个条件里多次引用pets或者要限制join的类型left join、inner join没有别名就非常痛苦。更麻烦的是老版Criteria里如果多次用字符串路径pets.nameHibernate内部会为同一个属性路径生成多个不同的隐式别名最后查出来的结果可能正确但生成的SQL又臭又长。3.2 Root.join与fetch join的别名到了JPA的CriteriaBuilder时代老朋友createAlias变成了Root.join。例如CriteriaBuilder cb entityManager.getCriteriaBuilder(); CriteriaQueryUser query cb.createQuery(User.class); RootUser root query.from(User.class); JoinUser, Pet petJoin root.join(pets, JoinType.LEFT); query.where(cb.equal(petJoin.get(name), 旺财));这里的petJoin就是路径的持柄它和Hibernate老API里的字符串别名不太一样它是类型安全的对象别名所有后续条件都通过petJoin.get(...)来写。好处是编译期能检查路径是否存在缺点是没有老版那么直观。真正的坑在fetch join上。root.fetch(pets, JoinType.LEFT)返回的是Fetch而不是Join但它也会在底层创建别名。如果你同时想对pets加条件还另外写了一个root.join(pets)Hibernate不会自动帮你把fetch的join和条件的join合并最后可能生成两个独立的join结果出现重复记录。我的处理习惯是需要过滤关联对象时只做join不加fetch只是为了避免N1查询时只用fetch不join。两条路尽量分开走别交叉。3.3 老坑路径字符串和别名覆盖这里说一个非常有代表性的老坑。旧版Criteria里createAlias(pets, p)之后再调用createAlias(pets.tags, t)第一次的p别名依然有效。但如果两次分别创建了指向同一路径的不同别名比如createAlias(pets, p)和createAlias(pets, pet)Hibernate会产生两个独立的join条件分别加到各自的别名上。这个行为看着没毛病却会导致结果集会比预期多出很多行。排查时看到SQL日志里出现两次对t_pet表的join第一反应就应该是某处重复创建了别名。另外字符串路径不存在时会抛org.hibernate.QueryException: could not resolve property: pets2 of: com.example.User。这类报错信息其实已经把问题点说得很清楚了但网络上的中文回答有时会绕到别名机制本身。我建议排查时直接把HQL或Criteria路径拆开逐级验证先确认User里有pets再确认Pet里有name基本十分钟内能定位。4. 原生SQL查询列别名是结果映射的唯一桥梁4.1 传统createSQLQuery映射到实体当查询需求用HQL或Criteria不好表达时我们会选择写原生SQL。原生SQL返回的是扁平的列Hibernate要靠“列名与属性名的对应关系”把行数据组装成实体。这个对应关系就是列别名。旧版Hibernate的经典写法是这样的Query query session.createSQLQuery( select u.id, u.name, u.age from t_user u where u.age 18) .addEntity(User.class); ListUser users query.list();这里u.id这一列Hibernate在没有任何列别名提示的情况下会拿列名id去匹配User实体标注的Column(name id)。一旦表字段和实体属性命名不一致比如数据库列叫user_name实体属性叫name就必须在SQL里显式起别名select u.user_name as name, u.age as age from t_user uas后面的别名name必须和实体User的属性名完全一致。这就是为什么很多老项目的原生SQL看起来特别啰嗦满眼都是as xxx——那不是作者有强迫症是被映射规则逼的。4.2 Spring Data JPA的nativeQuery与DTO投影现代项目用Spring Data JPA写原生SQL更常见。原生查询加DTO接口投影的组合值得单独说。例如public interface UserNameView { String getName(); Integer getAge(); } Query(value select u.user_name as name, u.age as age from t_user u, nativeQuery true) ListUserNameView findAllNames();这里的as name和as age就是Spring Data的代理对象用来生成接口方法返回值的依据。如果列别名和接口方法名对不上Spring会抛出转换异常说“无法找到名为name的属性”甚至直接返回null但不报错。我建议写这种查询的时候在注解下方保留一条SQL注释标明每个别名对应接口的哪个方法方便后来人维护。HQL版本里做DTO投影用的是select new原生SQL用的是别名匹配两种方式背后的理念完全不一样HQL是面向对象模型SQL是面向列结构。理解了这点你就不会在原生SQL里写select new com.example.DTO(...)了因为那个语法只在HQL里有效。4.3 标量查询让无名列变成有名字的列原生SQL里还有一种情况查询两个字段做统计比如select count(*) from t_user。这个列本身没有名字Hibernate返回的Object[]里只有一个元素类型是BigInteger或Long看日志时根本不知道对应什么。所以标量查询时给列起别名也有现实意义Query query session.createSQLQuery( select u.age as age, count(*) as cnt from t_user u group by u.age); ListObject[] rows query.list(); for (Object[] row : rows) { Integer age (Integer) row[0]; Long cnt (Long) row[1]; }对象数组里的下标顺序对应select子句里的列顺序这个不依赖别名。但如果你用了setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP)列别名age和cnt就会变成Map的key取数据时直接map.get(cnt)可读性就上来了。新版Hibernate里推荐用原生查询加NativeQuery接口配合ResultTransformer思路是一样的只是API换了壳。5. 别名踩坑速查表与排查思路5.1 别名问题速查表这些年处理过的Hibernate别名相关问题我整理成了一张速查表。碰到类似报错先对着查一下通常能省下半个小时试错。现象报错/表现原因处理方式HQL里写了表名QuerySyntaxException: t_user is not mapped把表名当实体名用了改成实体类名User原生SQL查询返回null字段字段为null但不报错列名与实体属性名不匹配在SQL里加as 实体属性名Criteria关联路径报错could not resolve property: xxx关联属性名拼写错误或不存在检查实体类字段名逐级验证查询结果出现大量重复行结果集行数远超预期同一路径重复创建别名导致多次join只创建一个别名并复用同一个引用DTO投影字段全null接口方法返回null列别名和DTO接口方法名不一致保持as别名与接口方法名一致聚合查询报错数据库提示“列必须出现在GROUP BY中”select的字段没有包含在group by检查select与group by字段清单这张表虽然简单但项目里真正出问题的场景绝大多数都能归到这几类中。5.2 一个实用的排查技巧最后分享一个排查技巧遇到任何疑似别名导致的查询问题第一件事不是看代码而是打开Hibernate的SQL日志输出。把hibernate.show_sqltrue打开找到那条被执行的SQL对照一下where条件、join数量和select列。举个例子如果你明明只想查5个用户Hibernate却多出了一次对宠物表的join日志里就能看到两个join t_pet这时候可以百分百断定是隐性重复join或别名覆盖问题。重点要看的是Hibernate生成的SQL别名像u1_0、p1_0这种与你定义的别名不是一回事。日志里出现u1_0不代表你的别名写法错了那只是Hibernate的“内部代号”。许多开发者在这里被误导以为自己写的别名导致SQL异常其实自己写的别名早就被解析成内部代号了。我个人在实际操作中的体会是写HQL前先给实体起好别名并且从第一条where到最后一条order都只引用这个别名绝不混用另一个临时称呼写原生SQL时凡是结果要映射回实体或DTO的列全部显式加as 目标属性名写Criteria时坚持用一个join变量贯穿查询不重复创建关联路径。这套规矩看着朴素但能避免掉绝大多数别名相关的坑比记住什么“隐式别名”“显式别名”的概念都管用。
返回列表