Spring Data JPA之三_多表操作

三 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删除主表记录时:

  1. 若主表有子表外键维护权,删除主表记录时,会自动将子表相关记录的外键字段置为NULL,然后删除主表记录;
  2. 若主表没有子表外键维护权,则无法直接删除主表记录,会报错;
  3. 不管有没有外键维护权,可以使用级联删除,将子表中相关记录也一并删除;实际开发中慎用。

注意: 主表中只要是唯一性索引就能作为子表的外键,不要求必须是主键。

级联操作:级联添加、级联删除。需要明确操作主体(具有外键维护权),且在其实体类的关系映射注解(如:@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为延迟加载。


版权声明:本文为IMchg原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。