聊聊JPA Criteria查询中的坑
JPA Criteria查询被称作动态安全类型查询,比JPQL这种方式更加健壮。关于JPA Criteria查询在IBM社区有一篇很好的文章,这里我就不去Copy(尊重默默为社区奉献的同行兄弟),请移步https://www.ibm.com/developerworks/cn/java/j-typesafejpa/
Bug复现
场景
创建一个Student
类,然后创建其StaticMetaModel Student_
类;然后使用Critirial查询年龄大于20的Student
。场景很简单,但是。。。结果很意外,让我们来看看相关配置和代码。
配置
依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kylin</groupId>
<artifactId>jpa-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jpa-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8
</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml配置文件
server:
port: 8800
spring:
datasource:
password: 123456
username: root
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/jpaadvance
jpa:
database-platform: mysql
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
代码
Student类代码
package com.kylin.jpademo.domain;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table
public class Student implements Serializable {
private static final long serialVersionUID = -7681363673194194734L;
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@Column(updatable = false, nullable = false)
private String id;
private String name;
private int age;
//geter and seter
Student_类代码
package com.kylin.jpademo.domain;
import com.kylin.jpademo.domain.metamodel.Student;//重点看这里
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@StaticMetamodel(Student.class)
public class Student_ {
public static volatile SingularAttribute<Student, String> id;
public static volatile SingularAttribute<Student, String> name;
public static volatile SingularAttribute<Student, Integer> age;
}
Repository接口
package com.kylin.jpademo.repository;
import com.kylin.jpademo.domain.metamodel.Student;
import java.util.List;
public interface StudentRepository {
List<Student> findStudentByAgeLessThan(int age);
}
Repository实现
package com.kylin.jpademo.repository.impl;
import com.kylin.jpademo.domain.metamodel.Student;
import com.kylin.jpademo.domain.Student_;
import com.kylin.jpademo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.List;
@Transactional
@Repository
public class StudentRepositoryImpl implements StudentRepository {
@Autowired
EntityManager em;
@Override
public List<Student> findStudentByAgeLessThan(int age) {
System.out.println(age);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
Root<Student> s = cq.from(Student.class); // from ...
Path<Integer> path = s.get(Student_.age);
Predicate condition = cb.gt(path, age); // condition: attribute > age
cq.where(condition); // where
TypedQuery<Student> tq = em.createQuery(cq);
return tq.getResultList();
}
}
执行findStudentByAgeLessThan
方法
如上图所示,报了一个NullPointerException,定位到了
StudentRepositoryImpl
类的这一行
Path<Integer> path = s.get(Student_.age);
尝试解决
Debug重新运行之后,发现Student_.age变量为空,说明StaticMetaModelStudent_
类并没有映射到Student
类。之后我google了很多方法去解决这个问题,都没有用。但是可以确定的是一定是StaticMetaModel出了问题,因为将上面出错那一行的代码改为"age"
之后,即:
Path<Integer> path = s.get(“age”);
Bug解决了,但这似乎违背了Criteria查询的宗旨,就是类型安全和避免运行时错误,这里的安全类型除了指明确查询中的类型安全之外,还有就是避免使用常量。如果"age"字符串手抖写成了“aeg”,在编译时肯定是没有问题的,只有在运行时才会暴露出来,到了那个时候为时已晚。所以为了尽可能的遵循Criteria的初衷,这种方式肯定不是很好的方案,之后笔者又尝试了很多种方案,都宣告失败。于是我删掉了Student_
类重新写了一次,但是这次Student
和Student_
位于同一包中,神奇的事情发生了,这次运行正确,并且成功查找出了年龄大于20的Student。
此时的工程结构
结果如下:
[
{
"id": "8a4ffa05622dcf1501622dd0aaa30003",
"name": "bob",
"age": 90
},
{
"id": "8a4ffa05622dcf1501622dd0ca410004",
"name": "an",
"age": 30
}
]
结论
Student
和Student_
必须位于同一包中。
更好的解决办法
解决之后,本人始终百思不得其解,google了很多文章也没有解决。在之后的各种尝试中,发现了自动生成StaticMetaModelStudent_
的方法,就是引入下面的依赖。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>1.1.1.Final</version>
</dependency>
此依赖可以自动创建metamodel。
现在我们删除我们自行创建的Student_
类,然后引入上述依赖,保持问题语句:
Path<Integer> path = s.get(Student_.age);
此时程序会出现编译错误:
因为刚才删除了
Student_
类,肯定会出现编译问题。抛开编译问题,这里我们继续执行,如下图所示,执行成功了。
那么为什么呢?总结刚才的操作,你可能会猜测是不是刚才引入的依赖自动创建了
Student_
呢?答案是肯定的。为了证实这一点,我们去看编译运行的结果:自动生成的Student_代码:
package com.kylin.jpademo.domain;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@StaticMetamodel(Student.class)
public abstract class Student_ {
public static volatile SingularAttribute<Student, String> name;
public static volatile SingularAttribute<Student, String> id;
public static volatile SingularAttribute<Student, Integer> age;
}
执行查询,没有问题。
可以看出自动生成的Student_
和Student
在同一包下,这也印证了刚才的做法。
结论
在Criteria查询中,Model和StaticMetaModel必须位于同一包下。因为在大型项目中,会涉及到很多Model,若不想自己创建对应的StaticMetaModel,可以使用hibernate-jpamodelgen
依赖,自动创建。