三 Spring Data JPA进阶
3.1 动态查询
上述查询中,查询条件固定,导致每个查询条件都对应一个专门的方法,如:根据名称查询、根据地址查询、根据年龄查询。动态查询,指可以将查询条件作为参数,动态地选择查询条件。动态查询基于JpaSpecificationExecutor接口。
3.1.1 动态查询的接口和类
JpaSpecificationExecutor包括的查询接口:
查询单个:findOne(Specification<T> spec),返回T
查询列表:findAll(Specification<T> spec),返回List<T>
分页查询:findAll(Specification<T> spec, Pageable pageable),返回Page<T>
结果排序:findAll(Specification<T> spec, Sort sort),返回List<T>
统计查询:count(Specification<T> spec),返回Long
Specification:查询条件接口
需要自定义Specification接口的实现类,实现方法:
Predicate toPredicate(Root\<T> root, CriteriaQuery<?> query, CriteriaDuilder cb);
root:查询的根对象,
CriteriaQuery:顶层查询对象,自定义查询方式,一般不使用;
CriteriaBuilder:查询的构造器,封装查询条件;
3.1.2 动态查询的API
动态查询步骤:
1.定义接口;
2.创建Specification实现类对象,并定义查询规则;
3.查询,获取结果;
接口:
public interface CustomerDao extends JpaRepository<Custormer,Long>,JpaSpecificationExecutor<Customer>{}
(1)findOne(Specification<T> spec)
调用
public void testSpec(){
// 匿名类的写法
Specificaiton<Customer> spec = new Specification<Customer>(){
// 实现toPredicate方法
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuild cd){
// 1.获取查询要比较的属性,返回值为path对象
Path<Obejct> custName = root.get("custName");
// 2.构造查询条件:第一个参数为要比较的属性(Path对象),第二个参数为属性值
Predicate predicate = cb.equal(custName,"zs");
return prediacate
}
}
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
多条件:
Specificaiton<Customer> spec = new Specification<Customer>(){
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuild cd){
Path<Obejct> custName = root.get("custName");
Path<Obejct> custId = root.get("custId");
Predicate p1= cb.equal(custName,"zs");
Predicate p2= cb.equal(custId ,"1");
Predicate p3= cb.and(p1,p2);
return p3;
}
}
(2)findAll(Specification<T> spec)
模糊查询:
equal比较方式可以直接以path对象为参数,其他比较方式(gt、lt、ge、le、like)要根据path指定比较的参数类型,指定方法:path.as(类型的字节码)
Specificaiton<Customer> spec = new Specification<Customer>(){
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuild cd){
Path<Obejct> custName = root.get("custName");
Predicate predicate = cb.like(custName.as(String.class),"zs");
return predicate;
}
}
(3)findAll(Specification<T> spec, Pageable)
Pageable:可分页接口,实现类为PageRequest;PageRequest构造函数需要两个参数,当前页数、每页数量
Page:分页对象,分页查询的返回值。Page对象中包括查询的各种信息。
Specification spec = null;
Pageable pageable = new PageRequest(0,2);
Page<Customer> page = customerDao.findAll(null,pageable)
System.out.println(page.getContent()); // 当前页数据
System.out.println(page.getTotalElements()); // 数据库表总条数
System.out.println(page.getTotalPages()); // 总页数
(4)findAll(Specification<T> spec, Sort sort)
排序
Specificaiton<Customer> spec = new Specification<Customer>(){
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuild cd){
Path<Obejct> custName = root.get("custName");
Predicate predicate = cb.like(custName.as(String.class),"zs");
return predicate;
}
}
// 排序对象:构造函数参数:
// 第一个参数:排序顺序 Sort.Direction.DES降序、Sort.Direction.AES升序
// 第二个参数:用于排序的属性名称
Sort sort = new Sort(Sort.Direction.DES, "custId");
List<Customer> list = customerDao.findAll(spec,sort);
(5)count(Specification<T> spec)
略
3.2 多表操作
3.2.1 表间关系
数据表的关系:
一对一:A表中的一条记录对应B表中的一条记录;
一对多:“一”的一方是主表,“多”的一方是从表;通过外键关联,从表以主表的主键作为外键;
多对多:通过中间表关联,中间表至少由两个字段组成,两个字段都是外键,对应两张表的主键,两个字段形成了联合主键;
实体类如何反映表间关系:
实体类通过包含关系反映表的一对多关系,即一个实体类中的某个成员变量类型是另一个实体类的Set;
Spring Data JPA多表操作流程:
1.明确表关系,以及关联方式,外键或中间表;
2.编写实体类,在实体类中反映表关系(通过注解);
3.建立关系;
注解配置多表关系流程:
1.声明关系:一对多@OneToMany、多对一@ManyToOne、多对多@ManyToMany
2.配置外键或中间表:
外键:Spring Data JPA中,主表和从表对应的类都可以配置外键信息,从而都可以控制从表中的外键;@JoinColumn(name = “对方表关联字段名”, referencedColumnName = “本表关联字段名”)
中间表:@JoinTable(name=“中间表名”,joinColumns=“本表与中间表外键映射情况”,inverseJoinColumns=“对方表与中间表外键映射情况”)
3.2.2 一对多关系操作
以客户(公司)和联系人为例,客户与联系人为一对多关系。在客户(一)和联系人(多)中都配置关联信息,则是双向关系,可通过客户找到联系人,也可通过联系人找到客户;如果只在一个中配置则是单向关系。
3.2.2.1 实体类与接口创建
主表实体类(维护外键)
@Entity
@Table(name="cst_customer")
public class Customer{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="cust_id")
private Long custId;
@Column(name="cust_name")
private String custName;
// 配置客户和联系人之间的关系(一对多关系)
@OneToMany(targetEntity = Contacter.class)
// @JoinColumn用于配置外键,name为从表中字段名,refer为主表中字段名
@JoinColumn(name = "con_id", referencedColumnName = "cust_id")
// 主表实体类通过维护set来建立联系
private Set<Contacter> contacter = new HashSet<>();
// getter、setter略
}
从表实体类(维护外键)
@Entity
@Table(name="cst_contacter")
public class Contacter{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="con_id")
private Long conId;
@Column(name="con_name")
private String conName;
// 配置联系人和客户之间的关系(多对一关系)
@ManyToOne(targetEntity = "Customer.class")
// name为从表中字段名,refer为主表中字段名
@JoinColumn(name="con_id", referencedColumnName = "cust_id");
// 从表实体类通过维护一个主表实体对象来建立联系
private Customer customer;
// getter、setter略
}
主表实体类(放弃外键维护的情况) 实际开发时采用此种方式
@Entity
@Table(name="cst_customer")
public class Customer{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="cust_id")
private Long custId;
@Column(name="cust_name")
private String custName;
// 只声明关联,不维护外键:使用mappedBy表明关系信息参照对方,值为对方用于表示关联的属性的属性名
@OneToMany(mappedBy = "customer")
// 主表实体类仍然通过维护set来建立联系,但联系直接参照从表维护的信息
private Set<Contacter> contacter = new Contacter<>();
// getter、setter略
}
接口:与单表查询相同
3.2.2.2 一对多操作
(1)保存
版本一:
通过客户创建外键:通过“一”的一方创建外键,会发送三条SQL:保存主表记录、保存从表记录、更新从表外键
@Transactional
@Rollback(false)
public void test(){
Customer customer = new Customer();
customer.setCustName("百度");
Contacter contacter = new Contacter();
contacter.setConName("zs");
// 主表实体类对象set中加入从表实体类。从而建立外键关系,且又主表维护
customer.getContacters().add(contacter);
customerDao.save();
contacterDao.save();
}
版本二:
通过联系人创建外键:通过“多”的一方创建外键,会发送两条SQL:保存主表记录、保存从表记录(包含外键字段)
Customer customer = ...
Contacter contacter = ...
// 从表属性中指定主表对象,从而建立关系且由从表维护
contacter.setContacter(contacter);
版本三:实际开发时,采用此种方式
双方均控制创建外键:结果同情况一,发送三条SQL;若客户放弃外键维护,则结果同情况二,发送两条SQL;
Customer customer = ...
Contacter contacter = ...
// 注意:Customer中对Contacter维护的是一个Set,不能直接set值,要先get后add
customer.getContacters().add(contacter);
contacter.setCustomer(customer);
(2) 删除/级联删除
由于主表的字段在子表中充当外键,主表记录在删除时可能导致异常,在使用Spring Data JPA删除主表记录时:
- 若主表有子表外键维护权,删除主表记录时,会自动将子表相关记录的外键字段置为NULL,然后删除主表记录;
- 若主表没有子表外键维护权,则无法直接删除主表记录,会报错;
- 不管有没有外键维护权,可以使用级联删除,将子表中相关记录也一并删除;实际开发中慎用。
注意: 主表中只要是唯一性索引就能作为子表的外键,不要求必须是主键。
级联操作:级联添加、级联删除。需要明确操作主体(具有外键维护权),且在其实体类的关系映射注解(如:@OneToMore)中加入级联Cascade属性。
实体类
@Entity
@Table(name="cst_customer")
public class Customer{
...
@OneToMany(mappedBy = "customer" cascade= CascadeType.ALL)
private Set<Contacter> contacter = new Contacter<>();
...
}
级联保存:添加客户时,添加相关联系人
@Transactional
@RollBack(false)
public void testCascadeAdd(){
Customer customer = new Customer();
customer.setCustName("百度");
Contact contacter = new Contacter();
contacter.setConName("zs");
customer.getContacters().add(contacter);
contacter.setCustomer(customer);
customerDAO.save(customer);
}
级联删除:删除客户时,删除相关联系人
@Transactional
@RollBack(false)
public void testCascadeAdd(){
Customer customer = customerDAO.findOne(1);
customerDAO.delete(customer);
}
3.2.3 多对多关系操作
3.2.3.1 实体类和接口创建
(1) 多对多实体关系配置注解:
1.声明表关系:@ManyToMany
@ManyToMany(targetEntity = 对方实体.class)
2.配置在中间表中的映射:@joinTable
@JoinTable(
name = "中间表名";
// joinColumns:当前表和中间表的映射情况
// @JoinColumn 外键配置:name中间表外键名,refer..本表列名
joinColumns = (@JoinColumn(name = "" , referencedColumnName = ""))
// joinColumns:对方表和中间表的映射情况
inverseJoinColumns = (@JoinColumn(name = "" , referencedColumnName = ""))
)
(2) 实体创建
Role实体类(维护中间表)
@Entity
@Table(name="sys_role")
public class Role{
@Id
@GeneratedValue(srategy = GenerationType.IDENTIFY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@MnayToMany(targetEntity = User.class)
@JoinTable(name = "sys_user_role",
joinColumns = (@JoinColumn(name = "sys_role_id"),referencedColumnName = "role_id")
inverseJoinColumns = (@JoinColumn(name = "sys_user_id"),referencedColumnName = "user_id")
)
private Set<User> users = new HashSet<>();
// getter、setter略
}
User实体类(维护中间表)
@Entity
@Table(name="sys_user")
public class Role{
@Id
@GeneratedValue(srategy = GenerationType.IDENTIFY)
@Column(name = "user_id")
private Long userId;
@Column(name = "user_name")
private String userName;
@MnayToMany(targetEntity = User.class)
@JoinTable(name = "sys_user_role",
joinColumns = (@JoinColumn(name = "sys_user_id"),referencedColumnName = "user_id")
inverseJoinColumns = (@JoinColumn(name = "sys_role_id"),referencedColumnName = "role_id")
)
private Set<User> roles = new HashSet<>();
// getter、setter略
}
Role实体类(放弃中间表维护权):实际开发中被选择的一方放弃中间表维护权
@Entity
@Table(name="sys_role")
public class Role{
@Id
@GeneratedValue(srategy = GenerationType.IDENTIFY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
// 放弃维护权,采用对方实体User的roles属性中的映射关系
@MnayToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
// getter、setter略
}
3.2.3.2 多对多操作
多对多操作封装的目标:操作实体类时,能自动维护中间表。
(1) 保存
版本一
User、Role都有维护权,User创建到Role的关系
@Transactional
@Rollback(false)
public void testAdd(){
User user = new User();
user.setUserName("zs");
Role role = new Role();
role.setRoleName("student");
// 创建user到role的关系,中间表中加入一条"z,s"记录
user.getRoles().add(role);
this.uesrDao.save();
this.roleDao.save();
}
版本二:
User、Role都有维护权,都创建到对方的关系
// 创建user到role的关系,中间表中加入一条"z,s"记录
user.getRoles().add(role);
// 创建user到role的关系,中间表中加入一条"z,s"记录
role.getUsers().add(user);
this.uesrDao.save();
this.roleDao.save();
由于中间表的两个外键构成联合主键,上述操作会导致主键重复,报错。
版本三:
Role放弃维护权,都创建到对方的关系
// 创建user到role的关系,中间表中加入一条"z,s"记录
user.getRoles().add(role);
// 创建user到role的关系,中间表中加入一条"z,s"记录
role.getUsers().add(user);
this.uesrDao.save();
this.roleDao.save();
正常运行,开发中采用此种方式
(2) 删除/级联删除
同一对多操作中的级联,先明确操作主体(有中间表维护权),然后在其实体类的关系映射注解@ManyToMany中加入Cascade属性。
多对多的级联操作会同时操作本表、对方表、中间表。代码与一对多操作完全相同。
3.3 多表查询(对象导航查询)
多表操作和单表操作调用的API名称以及操作步骤都是一样的,差异只在于实体类配置上,多表的实体类中用一个属性保存了所有相关对象,用来表示映射关系。当查询并返回多表对象时,自然也要找出这个属性的值,也就是所有相关对象。在得到这个属性值时,JPA进行了多表查询。也就是所谓的对象导航。对象导航:查询一个对象时,同时查找出相关对象。
JPA单元测试时,操作并不是在一个事务中完成可能会报错"No Session",加入@Transactional可解决
仍以一个Customer对应多个Contacter为例:
(1) 从"一"查"多"
getOne:延迟加载
public void testQuery(){
Customer customer = customerDao.getOne(1);
// 对象导航查询
Set<Contacter> contacters = customer.getContacters();
for(Contacter contacter:contacters){
System.out.println(contacter);
}
}
延迟加载改为立即加载:从"一"查"多"时,getOne、findOne都采用延迟加载,若想改为立即加载,需改变关系映射中的Fetch属性。如下:
一:Customer.java实体类
// Fetch属性:Lazy 延迟加载,EAGER 立即加载
@OneToMany(mappedBy="customer", fetch= FetchType.EAGER)
Set<Contacter> contacters = new HashSet<>();
二:调用
public void testQuery(){
Customer customer = customerDao.getOne(1);
// 对象导航查询
Set<Contacter> contacters = customer.getContacters();
for(Contacter contacter:contacters){
System.out.println(contacter);
}
}
(1) 从"多"查"一":
public void testQuery(){
Contacter contacter = Contecter.findOne(1);
// 对象导航查询
Customer customer = contacter.getCustomer();
System.out.println(contacter);
}
综上所述:
1.从"一"查"多",由于关联对象多,查找代价大,所以getOne、findOne都采用延迟加载;如果要改为立即加载,需要在实体类关系映射注解中配置Fetch属性。如:假设只想使用Customer本身,不关注其关联对象,此时显然不应该查找所有关联对象。
2.从"多"查"一",由于只关联一个对象,查找代价小,可延迟加载也可立即加载。保持findOne为立即加载,getOne为延迟加载。