上一篇介绍了入门基础篇SpringDataJPA访问数据库。本篇介绍SpringDataJPA进一步的定制化查询,使用JPQL或者SQL进行查询、部分字段映射、分页等。本文尽量以简单的建模与代码进行展示操作,文章比较长,包含查询的方方面面。如果能耐心看完这篇文章,你应该能使用SpringDataJPA应对大部分的持久层开发需求。如果你需要使用到动态条件查询,请查看下一篇博客,专题介绍SpringDataJPA的动态查询。
JPQL(JavaPersistence Query Language)是一种面向对象的查询语言,它在框架中最终会翻译成为sql进行查询,如果不知JPQL请大家自行谷歌了解一下,如果你会SQL,了解这个应该不废吹灰之力。
使用SpringDataJPA进行JPQL/SQL一般查询的核心是@Query注解,我们先来看看该注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@QueryAnnotation
@Documented
public @interface Query {
String value() default "";
String countQuery() default "";
String countProjection() default "";
boolean nativeQuery() default false;
String name() default "";
String countName() default "";
}
该注解使用的注解位置为方法、注解类型,一般我们用于注解方法即可。@QueryAnnotation标识这是一个查询注解;
@Query注解中有6个参数,value参数是我们需要填入的JPQL/SQL查询语句;nativeQuery参数是标识该查询是否为原生SQL查询,默认为false;countQuery参数为当你需要使用到分页查询时,可以自己定义(count查询)计数查询的语句,如果该项为空但是如果要用到分页,那么就使用默认的主sql条件来进行计数查询;name参数为命名查询需要使用到的参数,一般配配合@NamedQuery一起使用,这个在后面会说到;countName参数作用与countQuery相似,但是使用的是命名查询的(count查询)计数查询语句;countProjection为涉及到投影部分字段查询时的计数查询(count查询);关于投影查询,待会会说到。
有了@Query基础后,我们就可以小试牛刀一把了,对于jar包依赖,我们用的依旧是上一节的依赖,代码如下:
org.springframework.boot
spring-boot-starter-parent
1.4.1.RELEASE
UTF-8
1.8
org.springframework.boot
${springBoot.groupId}
spring-boot-starter-web
${springBoot.groupId}
spring-boot-starter-data-jpa
${springBoot.groupId}
spring-boot-starter-test
mysql
mysql-connector-java
junit
junit
4.12
项目结构如下:
JpaConfiguration配置类与上篇的相同:
@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@EnableTransactionManagement(proxyTargetClass=true)
@EnableJpaRepositories(basePackages={"org.fage.**.repository"})
@EntityScan(basePackages={"org.fage.**.entity"})
public class JpaConfiguration {
@Bean
PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor(){
return new PersistenceExceptionTranslationPostProcessor();
}
}
App类:
@SpringBootApplication
@ComponentScan("org.fage.**")
public class App {
public static void main(String[] args) throws Exception {
SpringApplication.run(App.class, args);
}
}
对于实体建模依旧用到上一篇所用的模型Department、User、Role,Department与User为一对多,User与Role为多对多,为了方便后面介绍投影,user多增加几个字段,代码如下:
@Entity
@Table(name = "user")
public class User implements Serializable {
private static final long serialVersionUID = -7237729978037472653L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String password;
@Column(name = "create_date")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Temporal(TemporalType.TIMESTAMP)
private Date createDate;
private String email;
// 一对多映射
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// 多对多映射
@ManyToMany @JsonBackReference
@JoinTable(name = "user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
@JoinColumn(name = "role_id") })
private List roles;
//getter and setter .....
}
@Entity
@Table(name = "department")
public class Department implements Serializable {
/**
*
*/
private static final long serialVersionUID = 3743774627141615707L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department")@JsonBackReference
@JsonBackReferenceprivate List users;
//getter and setter
}
@Entity
@Table(name="role")
public class Role implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1366815546093762449L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
//getter and setter
}
建模成功时,生成的表结构如下:
对于Repository:
@Repository
public interface DepartmentRepository extends JpaRepository{}
@Repository
public interface RoleRepository extends JpaRepository{}
@Repository
public interface UserRepository extends JpaRepository{
}
如果以上代码有看不懂的地方,请移步到上一篇一览基础篇。至此,我们已经将环境整理好了,至于表中的数据插入,希望各位参考上一篇文章进行基础的crud操作将表中数据进行填充,接下来介绍@Query查询
在UserRepository中增加以下方法:
//--------------JPQL查询展示-------------//
//展示位置参数绑定
@Query(value = "from User u where u.name=?1 and u.password=?2")
User findByNameAndPassword(String name, String password);
//展示名字参数绑定
@Query(value = "from User u where u.name=:name and u.email=:email")
User findByNameAndEmail(@Param("name")String name, @Param("email")String email);
//展示like模糊查询
@Query(value = "from User u where u.name like %:nameLike%")
List findByNameLike(@Param("nameLike")String nameLike);
//展示时间间隔查询
@Query(value = "from User u where u.createDate between :start and :end")
List findByCreateDateBetween(@Param("start")Date start, @Param("end")Date end);
//展示传入集合参数查询
@Query(value = "from User u where u.name in :nameList")
List findByNameIn(@Param("nameList")Collection nameList);
//展示传入Bean进行查询(SPEL表达式查询)
@Query(value = "from User u where u.name=:#{#usr.name} and u.password=:#{#usr.password}")
User findByNameAndPassword(@Param("usr")User usr);
//展示使用Spring自带分页查询
@Query("from User u")
Page findAllPage(Pageable pageable);
//展示带有条件的分页查询
@Query(value = "from User u where u.email like %:emailLike%")
Page findByEmailLike(Pageable pageable, @Param("emailLike")String emailLike);
TestClass的代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestClass {
final Logger logger = LoggerFactory.getLogger(TestClass.class);
@Autowired
UserRepository userRepository;
@Test
public void testfindByNameAndPassword(){
userRepository.findByNameAndPassword("王大帅", "123");
}
@Test
public void testFindByNameAndEmail(){
userRepository.findByNameAndEmail("张大仙", "[email protected]");
}
@Test
public void testFindByNameLike(){
List users = userRepository.findByNameLike("马");
logger.info(users.size() + "----");
}
@Test
public void testFindByCreateDateBetween() throws ParseException{
List users = userRepository.findByCreateDateBetween(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-01-01 00:00:00"), new Date(System.currentTimeMillis()));
logger.info(users.size() + "----");
}
@Test
public void testFindByNameIn(){
List list = new ArrayList();
list.add("王大帅");
list.add("李小三");
userRepository.findByNameIn(list);
}
@Test
public void testfindByNameAndPasswordEntity(){
User u = new User();
u.setName("李小三");
u.setPassword("444");
userRepository.findByNameAndPassword(u);
}
@Test
public void testFindAllPage(){
Pageable pageable = new PageRequest(0,5);
Page page = userRepository.findAllPage(pageable);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(page);
logger.info(json);
}
@Test
public void findByEmailLike(){
Pageable pageable = new PageRequest(0,5,new Sort(Direction.ASC,"id"));
userRepository.findByEmailLike(pageable, "@qq.com");
}
}
至此,显示了使用JPQL进行单表查询的绝大多数操作,当你在实体设置了fetch=FetchType.LAZY 或者EAGER时,会有不同的自动连接查询,鼓励大家自行尝试。以上查询语句有必要对其中几个进行解释一下;
对于UserRepository中的第一与第二个方法,目的是为了比较与展示位置绑定与名字绑定的区别,相信根据名称大家就能判别是什么意思与区别了,位置绑定即是方法参数从左到右第123456...所在位置的参数与查询语句中的第123456...进行对应。名字绑定即是查询语句中的参数名称与方法参数名称一一对应;对于第三个与第四个查询例子就不多说了;第五条查询语句展示的是传入集合进行in查询;第六条查询例子展示的是传入bean进行查询,该查询使用的表达式是Spring的SPEL表达式;
最后两条查询语句展示的是进行分页查询、分页并排序查询,使用的计数查询默认使用主查询语句中的条件进行count, 当Repository接口的方法中含有Pageable参数时,那么SpringData认为该查询是需要分页的;org.springframework.data.domain.Pageable是一个接口,接口中定义了分页逻辑操作,它具有一个间接实现类为PageRequest,我们最需要关注的是PageRequest这个实现类的三个构造方法:
public class PageRequest extends AbstractPageRequest {
....
....
public PageRequest(int page, int size) {
this(page, size, null);
}
public PageRequest(int page, int size, Direction direction, String... properties) {
this(page, size, new Sort(direction, properties));
}
public PageRequest(int page, int size, Sort sort) {
super(page, size);
this.sort = sort;
}
....
....
}
page参数为页码(查第几页,从0页开始),size为每页显示多少条记录数;Pageable与PageRequest的关系解释完了,那么就该介绍一下最后两条查询语句的返回值Page
{ "content": [
{ "id": 1,"name": "王大帅","password": "123", "createDate": 1515312688000, "email": "[email protected]","department": { "id": 1, "name": "开发部"}},
{ "id": 2, "name": "张大仙", "password": "456", "createDate": 1515139947000, "email": "[email protected]", "department": {"id": 1, "name": "开发部" }},
{"id": 3, "name": "李小三","password": "789","createDate": 1514794375000, "email": "[email protected]","department": {"id": 1, "name": "开发部" }},
{"id": 4, "name": "马上来","password": "444", "createDate": 1512116003000, "email": "[email protected]", "department": { "id": 1,"name": "开发部" } },
{ "id": 5, "name": "马德华", "password": "555","createDate": 1515312825000,"email": "[email protected]","department": { "id": 1, "name": "开发部"} }],
"last": true,
"totalPages": 1,
"totalElements": 5,
"size": 5,
"number": 0,
"sort": null,
"first": true,
"numberOfElements": 5
}
跟踪源码得到结论,Page到这里,Page与Pageable都了解了。
接下来介绍使用JPQL进行关联查询与部分字段映射。现在的查询需求是,查出所有用户的名字、用户所属部门、用户的email、统计用户所拥有的角色有多少个,然后将列表结果进行给前端显示。有的朋友说,那我把关联到的对象都拿出来不就完了。可是,实际开发中一个表下有几十个字段会很常见,如果全部都拿出来是没有必要的,所以我们可以把需要的字段拿出来就可以了,下面介绍两种方法实现这种需求。
我们在src/main/java中增加一个org.fage.vo包,该包下存放VO对象,我们在该包下创建一个UserOutputVO:
public class UserOutputVO {
private String name; //用户的名字
private String email; //用户的email
private String departmentName; //用户所属的部门
private Long roleNum; //该用户拥有的角色数量
public UserOutputVO(String name, String email, String departmentName, Long roleNum) {
super();
this.name = name;
this.email = email;
this.departmentName = departmentName;
this.roleNum = roleNum;
}
public UserOutputVO() {
super();
}
//getter and setter and toString
...
}
在UserRepository中创建查询方法:
@Query(value = "select new org.fage.vo.UserOutputVO(u.name, u.email, d.name as departmentName, count(r.id) as roleNum) from User u "
+ "left join u.department d left join u.roles r group by u.id")
Page findUserOutputVOAllPage(Pageable pageable);
这里注意一下,VO中的构造方法参数一定要与查询语句中的查询字段类型相匹配(包括数量),如果不匹配就会报错。以下是测试代码:
@Test
public void testFindUserOutputVOAllPage(){
Pageable pageable = new PageRequest(0,5);
Page page = userRepository.findUserOutputVOAllPage(pageable);
List list = page.getContent();
for(UserOutputVO vo : list)
logger.info(vo.toString());
}
输出结果:
对于连接查询,有join、left join 、right join,与sql的类似,但是唯一需要注意的地方就是建模的关系要能连接起来,因为只有这样才能使用“.”进行连接;就像你想的那样,它是类似对象导航的,与sql的表连接有些使用上的不同,但是最终的连接结果是相同的。
public interface UserProjection {
String getName();
@Value("#{target.emailColumn}")//当别名与该getXXX名称不一致时,可以使用该注解调整
String getEmail();
String getDepartmentName();
Integer getRoleNum();
}
在UserRepository中创建查询语句:
//故意将email别名为emailColumn,以便讲解@Value的用法
@Query(value = "select u.name as name, u.email as emailColumn, d.name as departmentName, count(r.id) as roleNum from User u "
+ "left join u.department d left join u.roles r group by u.id")
Page findUserProjectionAllPage(Pageable pageable);
在TestClass中添加测试方法:
@Test
public void testFindUserProjectionAllPage(){
Page page = userRepository.findUserProjectionAllPage(new PageRequest(0,5));
Collection list = page.getContent();
for(UserProjection up : list){
logger.info(up.getName());
logger.info(up.getEmail());
logger.info(up.getDepartmentName());
logger.info(up.getRoleNum()+"");
}
}
测试结果是成功的。在这里需要注意几点约束,Projection接口中必须以“getXXX”来命名方法,关于“XXX”则是要与查询语句中的别名相对应,注意观察上面的Projection接口与查询语句就发现了。不难发现,有一个别名为emailColumn,与Projection接口中的getEmail方法并不对应,这种时候可以使用@Value{"${target.xxx}"}注解来调整,注意其中的target不能省略,可以把target看成用别名查出来的临时对象,这样就好理解了。
两种方式都可以,对于到底哪种方式好,这取决于你的需求。
@Entity
@Table(name="role")
@NamedQueries({
@NamedQuery(name = "Role.findById", query = "from Role r where r.id=?1"),
@NamedQuery(name = "Role.findAllPage", query = "from Role r")
//...更多的@NamedQuery
})
public class Role implements Serializable{
private static final long serialVersionUID = 1366815546093762449L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
public Role(){
super();
}
public Role(String name){
this.name = name;
}
//getter and setter
}
对应的RoleRepository代码:
@Repository
public interface RoleRepository extends JpaRepository{
Role findById(Long id);
Page findAllPage(Pageable pageable);
}
相应的测试代码:
@Test
public void testFindRoleById(){
roleRepository.findById(1l);
}
@Test
public void testFindRoleAllPage(){
roleRepository.findAll(new PageRequest(0,5));
}
以上就是命名查询的常用方式。
有些时候,JPQL使用不当会导致转化成的sql并不如理想的简洁与优化,所以在特定场合还是得用到原生SQL查询的,比如当你想优化sql时等等。
使用原生查询时用的也是@Query注解,此时nativeQuery参数应该设置为true。我们先来看一些简单的查询
@Query(value = "select * from user u where u.id=:id", nativeQuery = true)
User findByIdNative(@Param("id")Long id);
@Query(value = "select * from user", nativeQuery = true)
List findAllNative();
看看测试代码:
@Test
@Transactional
public void testFindByIdNative(){
User u = userRepository.findByIdNative(1l);
logger.info(u.toString());
logger.info(u.getRoles().toString());
}
@Test
public void testFindAllNative(){
List list = userRepository.findAllNative();
for(User u : list){
logger.info(u.toString());
}
}
结果发现当查所有字段的时候,确实能映射成功,并且fetch快加载、懒加载自动关联也能正常使用。接下来我们换刚才使用JPQL时的查询需求,看看用SQL时该怎么做。
查询列表的需求依旧是刚才介绍使用JPQL时使用的需求(分页查出所有用户的名字、用户所属部门、用户的email、统计用户所拥有的角色有多少个),在UserRepository中创建代码片段:
//展示原生查询
@Query(value = "select u.name as name, u.email as emailColumn, d.name as departmentName, count(ur.role_id) as roleNum from user u "
+ "left join department d on d.id=u.department_id left join user_role ur on u.id=ur.user_id group by u.id limit :start,:size",
nativeQuery = true)
List
在TestClass中创建测试代码:
@Test
public void testFindUserProjectionAllPageNative(){
Pageable pageable = new PageRequest(0,5);
List
解释一下上面代码,由于是原生查询不支持动态分页,Page分页我们只能自己做了,但是依旧使用的是Spring的Page;pageable.getOffset()与pageable.getPageSize()分别对应limit ?, ?的第一与第二个问号。原生查询得出来的List是包函一堆被封装成Object的对象数组,每个object数组可以通过数组索引拿出值,也就与需要查的字段一一对应。如果你需要存入VO再带回给前端,那么你可以自行封装。对于PageImpl,我们使用了public PageImpl(List
当你需要进行sql优化时,可能用原生sql方式会更好。但是一般需求时候用JPQL还是比较方便的,毕竟这样比较省事,拿数据总是需要分页的,有时候只需要拿几个字段也是这样。
当你在接到一般需求时,使用JPQL的方式其实已经足够用了。但是如果对sql需要优化的时候,你也可以使用SQL的方式。总而言之,需要根据需求来应变使用的策略。
如果文中有不当的地方欢迎同学们提出建议与修改方案,但是请不要谩骂与辱骂。
下一篇将讲解SpringDataJPA根据动态条件进行查询的方方面面。