在前面的章节已经讲述了SpringDataJpa的CRUD操作以及其底层代理实现的分析,下面介绍SpringDataJpa中的复杂查询和动态查询,多表查询。(保姆级教程)
文章字数较多,请各位按需阅读。
不清楚JPA的小伙伴可以参考这篇文章:JPA简介;
不清楚SpringDataJPA环境搭建的小伙伴可以参考这篇文章:SpringDataJPA入门案例;
想了解SpringDataJPA代理类实现过程可以参考这篇文章:SpringDadaJPA底层实现原理
如需转载,请注明出处。
1.复杂查询
i.方法名称规则查询
方法名查询:只需要按照SpringDataJpa提供的方法名称规则去定义方法,在dao接口中定义方法即可。
其中对于方法的名称有一套约定。
KeyWord | Sample | JPQL |
---|---|---|
And | findByLastnameAndFirstname | where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | where x.lastname = ?1 or x.firstname = ?2 |
Between | findByAgeBetween | where x.Age between ?1 and ?2 |
LessThan | findByAgeLessThan | where x.age < ?1 |
GreaterThan | findByAgeGreaterThan | where x.age > ?1 |
Like | findByFirstnameLike | where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | where x.firstname not like ?1 |
TRUE | findByActiveTrue() | where x.active = true |
FALSE | findByActiveFalse() | where x.active = false |
1 | 复制代码public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> { |
ii.JPQL查询
使用 Spring Data JPA 提供的查询方法已经可以解决大部分的应用场景,但是对于某些业务来
说,我们还需要灵活的构造查询条件,这时就可以使用@Query 注解,结合 JPQL 的语句方式完成
查询 。
@Query 注解的使用非常简单,只需在方法上面标注该注解,同时提供一个 JPQL 查询语句即可
注意:
通过使用 @Query 来执行一个更新操作,为此,我们需要在使用 @Query 的同时,用 @Modifying 来将该操作标识为修改查询,这样框架最终会生成一个更新的操作,而非查询 。
1 | 复制代码public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> { |
注意:在执行springDataJpa中使用jpql完成更新,删除操作时,需要手动添加事务的支持 必须的;因为默认会执行结束后,回滚事务。
1 | 复制代码 @Test |
iii.SQL查询
Spring Data JPA 同样也支持 sql 语句的查询,如下:
1 | 复制代码/** |
2.动态查询
springdatajpa的接口规范:
- JpaRepository<操作的实体类型,实体类型中的 主键 属性的类型>
封装了基本的CRUD的操作,分页等;
- JpaSpecificationExecutor<操作的实体类类型>
封装了复杂查询。
上述查询方法使用到的是接口JpaRepository中的方法,下面分析JpaSpecificationExecutor中的方法。
i.为什么需要动态查询
可能有些许疑惑,为什么还需要动态查询呢?有时候我们在查询某个实体的时候哦,给定的查询条件不是固定的,这个时候就需要动态构建相应的查询语句,可以理解为上述的查询条件是定义在dao接口中的,而动态查询条件定义在实现类中。
ii.JpaSpecificationExecutor中定义的方法
1 | 复制代码public interface JpaSpecificationExecutor<T> { |
在上述方法中,我们可以看到接口Specification。可以简单理解为,Specification构造的就是查询条件。我们看看Specification中定义的方法。
1 | 复制代码/* |
与上述查询方法不同,复杂查询定义在dao接口中,而动态查询定义在实现类中。
1)单条件查询
1 | 复制代码@RunWith(SpringJUnit4ClassRunner.class) |
2)多条件查询
1 | 复制代码@RunWith(SpringJUnit4ClassRunner.class) |
3)模糊查询
1 | 复制代码@RunWith(SpringJUnit4ClassRunner.class) |
4)分页查询
1 | 复制代码@RunWith(SpringJUnit4ClassRunner.class) |
5)对查询结果进行排序
1 | 复制代码@RunWith(SpringJUnit4ClassRunner.class) |
3.多表查询
上述复杂查询和动态查询都是基于单表查询,只需要指定实体类与数据库表中一对一的映射。而多表查询需要修改实体类之间的映射关系。
在数据库中表与表之间,存在三种关系:多对多、一对多、一对一。
那么与之对应的实体映射也应该有三种关系。那么在JPA中表的关系如何分析呢?
1.建立表与表之间的关系
- 第一步:首先确定两张表之间的关系。
如果关系确定错了,后面做的所有操作就都不可能正确。 - 第二步:在数据库中实现两张表的关系
- 第三步:在实体类中描述出两个实体的关系
- 第四步:配置出实体类和数据库表的关系映射(重点)
4.JPA中的一对多
案例分析:
采用两个实体对象:公司与员工
在不考虑兼职的情况下,每名员工对应一家公司,每家公司有多名员工。
在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对
多的关系,需要使用数据库的外键约束。
**什么是外键?**指的是从表中有一列,取值参照主表中的主键,这一列就是外键。
数据库表:
1 | 复制代码CREATE TABLE `cst_customer` ( |
1.建立实体与表之间的映射关系
注意:使用的注解都是JPA规范的,导包需要导入javac.persistence下的包
1 | 复制代码package ctgu.pojo; |
1 | 复制代码package ctgu.pojo; |
注意:在上述实体中,均对外键进行了维护。
2.映射的注解说明
i.@OneToMany
作用:建立一对多的关系映射
属性:
- targetEntityClass:指定多的多方的类的字节码(常用)
- mappedBy:指定从表实体类中引用主表对象的名称。(常用)
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- orphanRemoval:是否使用孤儿删除
ii.@ManyToOne
作用:建立多对一的关系
属性:
- targetEntityClass:指定一的一方实体类字节码(常用)
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- optional:关联是否可选。如果设置为 false,则必须始终存在非空关系。
iii.@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
- name:指定外键字段的名称(常用)
- referencedColumnName:指定引用主表的主键字段名称(常用)
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
3.一对多测试
i.保存公司和联系人
1 | 复制代码package ctgu.OntoMany; |
运行结果:
1 | 复制代码Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?) |
分析:
执行了两条insert语句以及一条update语句,有一条update的语句是多余的。产生这种现象的原因是:我们在两个实体类中均对外键进行了维护,相当于维护了两次,解决的办法是放弃一方的维权。
修改:将主表中的关系映射修改为:
1 | 复制代码 @OneToMany(mappedBy = "customer",cascade = CascadeType.ALL,fetch = FetchType.EAGER) |
ii.级联添加
级联操作:操作一个对象同时操作它的关联对象
使用方法:只需要在操作主体的注解上配置casade
1 | 复制代码 /** |
一般是对配置在主表中,但是:注意:慎用CascadeType.ALL
1 | 复制代码 package ctgu.OntoMany; |
测试结果:
1 | 复制代码Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?) |
iii.级联删除
删除公司的同时,删除对应公司的所有员工。
JPA中删除是先执行查询再执行删除。
1 | 复制代码 /** |
测试结果:
1 | 复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=? |
注意:一般使用级联删除是比较危险的,在一对多的情况下。如果没有使用级联操作,应该如何删除数据?
只删除从表数据:可以任意删除。
删除主表数据:
- 有从表数据
- 在默认情况下,会将外键字段置为null,然后再执行删除。此时如果从表的结构上,外键字段存在非空约束将会报错。
- 使用级联删除。
- 应该先根据外键值,删除从表中的数据,再删除主表中的数据。
- 没有从表数据:随便删
iv.一对多删除(非级联删除)
创建方法:根据customer删除员工。(使用复杂查询中的自定义方法)
1 | 复制代码package ctgu.dao; |
此时的主表的关键映射为设置级联操作:
1 | 复制代码 @OneToMany(mappedBy = "customer",fetch = FetchType.EAGER) |
测试:
1 | 复制代码 package ctgu.OntoMany; |
测试结果:
1 | 复制代码Hibernate: select linkman0_.lkm_id as lkm_id1_1_, linkman0_.lkm_cust_id as lkm_cust9_1_, linkman0_.lkm_email as lkm_emai2_1_, linkman0_.lkm_gender as lkm_gend3_1_, linkman0_.lkm_memo as lkm_memo4_1_, linkman0_.lkm_mobile as lkm_mobi5_1_, linkman0_.lkm_name as lkm_name6_1_, linkman0_.lkm_phone as lkm_phon7_1_, linkman0_.lkm_position as lkm_posi8_1_ from cst_linkman linkman0_ left outer join cst_customer customer1_ on linkman0_.lkm_cust_id=customer1_.cust_id where customer1_.cust_id=? |
5.JPA中的多对多
案例:用户和角色。
用户:指社会上的某个人。
角色:指人们可能有多种身份信息
比如说:小明有多种身份,即使java工程师,还是后端攻城狮,也是CEO;而Java工程师除了小明,还有张三、李四等等。
所以我们说,用户和角色之间的关系是多对多。
1.建立实体类与表直接的关系映射
1 | 复制代码package ctgu.pojo; |
1 | 复制代码package ctgu.pojo; |
2,映射注解说明
i.@ManyToMany
作用:用于映射多对多关系
属性:
- cascade:配置级联操作。
- fetch:配置是否采用延迟加载。
- targetEntity:配置目标的实体类。映射多对多的时候不用写。
- mappedBy:指定从表实体类中引用主表对象的名称。(常用)
ii.@JoinTable
作用:针对中间表的配置
属性:
- nam:配置中间表的名称
- joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
- inverseJoinColumn:中间表的外键字段关联对方表的主键字段
iii.@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
- name:指定外键字段的名称
- referencedColumnName:指定引用主表的主键字段名称
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
3.多对多测试
i.保存用户和角色
数据库表:(其实可以直接由springdataJPA自动生成)
1 | 复制代码CREATE TABLE `sys_user` ( |
dao接口:
1 | 复制代码package ctgu.dao; |
1 | 复制代码package ctgu.dao; |
测试案例:
1 | 复制代码package ctgu; |
测试结果:
1 | 复制代码org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement |
原因:
在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,
中间表的 2 个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一
方放弃对中间表的维护权即可,推荐在被动的一方放弃,配置如下:
1 | 复制代码//放弃对中间表的维护权,解决保存中主键冲突的问题 |
正确结果:
1 | 复制代码Hibernate: insert into sys_user (age, user_name) values (?, ?) |
系统会自动创建表sys_user_role并添加数据。
ii.级联保存
保存用户的同时,保存其关联角色。
只需要在操作对象的注解上配置cascade
1 | 复制代码@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL) |
1 | 复制代码package ctgu; |
测试结果:
1 | 复制代码Hibernate: insert into sys_role (role_name) values (?) |
iii.级联删除
1 | 复制代码 /** |
测试结果:
1 | 复制代码Hibernate: select role0_.role_id as role_id1_0_0_, role0_.role_name as role_nam2_0_0_ from sys_role role0_ where role0_.role_id=? |
注意:
- 调用的对象是role,所有需要在role对象中配置级联cascade = CascadeType.ALL;
- 慎用!可能会清空相关联的数据;
6.SpringDataJPA中的多表查询
以下例子采用一对多的案例实现。
i.对象导航查询
对象导航查询的方式就是根据已加载的对象,导航到他的关联对象。利用实体与实体之间的关系来检索对象。例如:通过ID查询出一个Customer,可以调用Customer对象中的getLinkMans()方法来获取该客户的所有联系人。
对象导航查询使用的要求是:两个对象之间必须存在关联联系。
案例:查询公司,获取公司下所有的员工
1 | 复制代码package ctgu.QueryTest; |
问题:我们在查询Customer时,一定要把LinkMan查出来吗?
分析:如果我们不查的话,在需要的时候需要重新写代码,调用方法查询;但是每次都查出来又会浪费服务器的内存。
解决:查询主表对象时,采用延迟加载的思想,通过配置的方式,当我们需要使用的时候才查询。
延迟加载
由于上述调用的对象为Customer,故而在Customer对象中需要配置延迟加载。Customer对象
1 | 复制代码@OneToMany(mappedBy = "customer",fetch = FetchType.LAZY) |
测试结果:
1 | 复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_ from cst_customer customer0_ where customer0_.cust_id=? |
分析:我们发现其执行了两条select语句。
问题:在我们查LinkMan时,是否需要把Customer查出来?
分析:由于一个用户只属于一家公司,及每个LinkMan都有唯一的Customer与之对应。如果我们不查,在使用的时候需要额外代码查询。且查询出的是单个对象,对内存消耗较小。
解决:在从表中采用立即加载的思想,只要查询从表实体,就把主表对象同时查出来。
立即加载
1 | 复制代码 @OneToMany(mappedBy = "customer",fetch = FetchType.EAGER) |
测试结果:
1 | 复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=? |
分析结果:我们发现其只执行了一条select语句。
对比可以发现,立即加载是一次性将查询对象以及关联对象查出来,而延迟加载是先查询目标对象,如果未调用
Set<LinkMan> linkMans = customer.getLinkMans();
方法,则将不会执行关联对象的查询。
ii.使用 Specification 查询
1 | 复制代码/** |
本文转载自: 掘金