SpringDataJpa(三)(上)+https://developer.aliyun.com/article/1556647
9.5 一对多的操作
9.5.1 添加
test/OneToManyTest.java
package cn.itcast.test; import cn.itcast.dao.CustomerDao; import cn.itcast.dao.LinkManDao; import cn.itcast.domain.Customer; import cn.itcast.domain.LinkMan; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class OneToManyTest { @Autowired private CustomerDao customerDao; @Autowired private LinkManDao linkManDao; /** * 保存一个客户,保存一个联系人 * 效果:客户和联系人作为独立的数据保存到数据库中 * 联系人的外键为空 * 原因? * 实体类中没有配置关系 */ @Test @Transactional //配置事务 @Rollback(false) //不自动回滚 public void testAdd() { //创建一个客户,创建一个联系人 Customer customer = new Customer(); customer.setCustName("百度"); LinkMan linkMan = new LinkMan(); linkMan.setLkmName("小李"); /** * 配置了客户到联系人的关系 * 从客户的角度上:发送两条insert语句,发送一条更新语句更新数据库(更新外键) * 由于我们配置了客户到联系人的关系:客户可以对外键进行维护 */ customer.getLinkMans().add(linkMan); customerDao.save(customer); linkManDao.save(linkMan); } @Test @Transactional //配置事务 @Rollback(false) //不自动回滚 public void testAdd1() { //创建一个客户,创建一个联系人 Customer customer = new Customer(); customer.setCustName("百度"); LinkMan linkMan = new LinkMan(); linkMan.setLkmName("小李"); /** * 配置联系人到客户的关系(多对一) * 只发送了两条insert语句 * 由于配置了联系人到客户的映射关系(多对一) * * */ linkMan.setCustomer(customer); customerDao.save(customer); linkManDao.save(linkMan); } /** * 会有一条多余的update语句 * * 由于一的一方可以维护外键:会发送update语句 * * 解决此问题:只需要在一的一方放弃维护权即可 * */ @Test @Transactional //配置事务 @Rollback(false) //不自动回滚 public void testAdd2() { //创建一个客户,创建一个联系人 Customer customer = new Customer(); customer.setCustName("百度"); LinkMan linkMan = new LinkMan(); linkMan.setLkmName("小李"); linkMan.setCustomer(customer);//由于配置了多的一方到一的一方的关联关系(当保存的时候,就已经对外键赋值) customer.getLinkMans().add(linkMan);//由于配置了一的一方到多的一方的关联关系(发送一条update语句) customerDao.save(customer); linkManDao.save(linkMan); } /** * 级联添加:保存一个客户的同时,保存客户的所有联系人 * 需要在操作主体的实体类上,配置casacde属性 */ @Test @Transactional //配置事务 @Rollback(false) //不自动回滚 public void testCascadeAdd() { Customer customer = new Customer(); customer.setCustName("百度1"); LinkMan linkMan = new LinkMan(); linkMan.setLkmName("小李1"); linkMan.setCustomer(customer); customer.getLinkMans().add(linkMan); customerDao.save(customer); } /** * 级联删除: * 删除1号客户的同时,删除1号客户的所有联系人 */ @Test @Transactional //配置事务 @Rollback(false) //不自动回滚 public void testCascadeRemove() { //1.查询1号客户 Customer customer = customerDao.findOne(1l); //2.删除1号客户 customerDao.delete(customer); } }
通过保存的案例,我们可以发现在设置了双向关系之后,会发送两条insert语句,一条多余的update语句,那我们的解决是思路很简单,就是一的一方放弃维护权
Customer.java
/** *放弃外键维护权的配置将如下配置改为 */ //@OneToMany(targetEntity=LinkMan.class) //@JoinColumn(name="lkm_cust_id",referencedColumnName="cust_id") //设置为 @OneToMany(mappedBy="customer")
9.5.2 删除
@Autowired private CustomerDao customerDao; @Test @Transactional @Rollback(false)//设置为不回滚 public void testDelete() { customerDao.delete(1l); }
删除操作的说明如下:
删除从表数据:可以随时任意删除。
删除主表数据:
- 有从表数据
1、在默认情况下,它会把外键字段置为null,然后删除主表数据。如果在数据库的表 结构上,外键字段有非空约束,默认情况就会报错了。
2、如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null, 没有关系)因为在删除时,它根本不会去更新从表的外键字段了。
3、如果还想删除,使用级联删除引用 - 没有从表数据引用:随便删
在实际开发中,级联删除请慎用!(在一对多的情况下)
9.5.3 级联操作
级联操作:指操作一个对象同时操作它的关联对象
使用方法:只需要在操作主体的注解上配置cascade
/** * cascade:配置级联操作 * CascadeType.MERGE 级联更新 * CascadeType.PERSIST 级联保存: * CascadeType.REFRESH 级联刷新: * CascadeType.REMOVE 级联删除: * CascadeType.ALL 包含所有 */ @OneToMany(mappedBy="customer",cascade=CascadeType.ALL)
test/ObjectQueryTest.java
package cn.itcast.test; import cn.itcast.dao.CustomerDao; import cn.itcast.dao.LinkManDao; import cn.itcast.domain.Customer; import cn.itcast.domain.LinkMan; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import java.util.Set; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class ObjectQueryTest { @Autowired private CustomerDao customerDao; @Autowired private LinkManDao linkManDao; //could not initialize proxy - no Session //测试对象导航查询(查询一个对象的时候,通过此对象查询所有的关联对象) @Test @Transactional // 解决在java代码中的no session问题 public void testQuery1() { //查询id为1的客户 Customer customer = customerDao.getOne(1l); //对象导航查询,此客户下的所有联系人 Set<LinkMan> linkMans = customer.getLinkMans(); for (LinkMan linkMan : linkMans) { System.out.println(linkMan); } } /** * 对象导航查询: * 默认使用的是延迟加载的形式查询的 * 调用get方法并不会立即发送查询,而是在使用关联对象的时候才会差和讯 * 延迟加载! * 修改配置,将延迟加载改为立即加载 * fetch,需要配置到多表映射关系的注解上 * */ @Test @Transactional // 解决在java代码中的no session问题 public void testQuery2() { //查询id为1的客户 Customer customer = customerDao.findOne(1l); //对象导航查询,此客户下的所有联系人 Set<LinkMan> linkMans = customer.getLinkMans(); System.out.println(linkMans.size()); } /** * 从联系人对象导航查询他的所属客户 * * 默认 : 立即加载 * 延迟加载: * */ @Test @Transactional // 解决在java代码中的no session问题 public void testQuery3() { LinkMan linkMan = linkManDao.findOne(2l); //对象导航查询所属的客户 Customer customer = linkMan.getCustomer(); System.out.println(customer); } }
十、JPA中的多对多
10.1 示例分析
我们采用的示例为用户和角色。
用户:指的是咱们班的每一个同学。
角色:指的是咱们班同学的身份信息。
比如A同学,它是我的学生,其中有个身份就是学生,还是家里的孩子,那么他还有个身份是子女。
同时B同学,它也具有学生和子女的身份。
那么任何一个同学都可能具有多个身份。同时学生这个身份可以被多个同学所具有。
所以我们说,用户和角色之间的关系是多对多。
10.2 表关系建立
多对多的表关系建立靠的是中间表,其中用户表和中间表的关系是一对多,角色表和中间表的关系也是一对多,如下图所示:
10.3 实体类关系建立以及映射配置
代码结构如下:
一个用户可以具有多个角色,所以在用户实体类中应该包含多个角色的信息,代码如下:
daomain/User.java
package cn.itcast.domain; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "sys_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="user_id") private Long userId; @Column(name="user_name") private String userName; @Column(name="age") private Integer age; /** * 配置用户到角色的多对多关系 * 配置多对多的映射关系 * 1.声明表关系的配置 * @ManyToMany(targetEntity = Role.class) //多对多 * targetEntity:代表对方的实体类字节码 * 2.配置中间表(包含两个外键) * @JoinTable * name : 中间表的名称 * joinColumns:配置当前对象在中间表的外键 * @JoinColumn的数组 * name:外键名 * referencedColumnName:参照的主表的主键名 * inverseJoinColumns:配置对方对象在中间表的外键 */ @ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL) @JoinTable(name = "sys_user_role", //joinColumns,当前对象在中间表中的外键 joinColumns = {@JoinColumn(name = "sys_user_id",referencedColumnName = "user_id")}, //inverseJoinColumns,对方对象在中间表的外键 inverseJoinColumns = {@JoinColumn(name = "sys_role_id",referencedColumnName = "role_id")} ) private Set<Role> roles = new HashSet<>(); public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } }
一个角色可以赋予多个用户,所以在角色实体类中应该包含多个用户的信息,代码如下:
daomain/Role.java
package cn.itcast.domain; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "sys_role") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "role_id") private Long roleId; @Column(name = "role_name") private String roleName; //配置多对多 @ManyToMany(mappedBy = "roles") //配置多表关系 private Set<User> users = new HashSet<>(); public Long getRoleId() { return roleId; } public void setRoleId(Long roleId) { this.roleId = roleId; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } }
10.4 映射的注解说明
@ManyToMany
作用:用于映射多对多关系
属性:
cascade
:配置级联操作。
fetch:配置是否采用延迟加载。
targetEntity
:配置目标的实体类。映射多对多的时候不用写。
@JoinTable
作用:针对中间表的配置
属性:
nam:配置中间表的名称
joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
inverseJoinColumn:中间表的外键字段关联对方表的主键字段
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
10.5 多对多的操作
10.5.1 保存
test/ManyToManyTest.java
package cn.itcast.test; import cn.itcast.dao.RoleDao; import cn.itcast.dao.UserDao; import cn.itcast.domain.Role; import cn.itcast.domain.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext.xml") public class ManyToManyTest { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; /** * 保存一个用户,保存一个角色 * * 多对多放弃维护权:被动的一方放弃 */ @Test @Transactional @Rollback(false) public void testAdd() { User user = new User(); user.setUserName("小李"); Role role = new Role(); role.setRoleName("java程序员"); //配置用户到角色关系,可以对中间表中的数据进行维护 1-1 user.getRoles().add(role); //配置角色到用户的关系,可以对中间表的数据进行维护 1-1 role.getUsers().add(user); userDao.save(user); roleDao.save(role); } //测试级联添加(保存一个用户的同时保存用户的关联角色) @Test @Transactional @Rollback(false) public void testCasCadeAdd() { User user = new User(); user.setUserName("小李"); Role role = new Role(); role.setRoleName("java程序员"); //配置用户到角色关系,可以对中间表中的数据进行维护 1-1 user.getRoles().add(role); //配置角色到用户的关系,可以对中间表的数据进行维护 1-1 role.getUsers().add(user); userDao.save(user); } /** * 案例:删除id为1的用户,同时删除他的关联对象 */ @Test @Transactional @Rollback(false) public void testCasCadeRemove() { //查询1号用户 User user = userDao.findOne(1l); //删除1号用户 userDao.delete(user); } }
在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,中间表的2个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一方放弃对中间表的维护权即可,推荐在被动的一方放弃,配置如下:
//放弃对中间表的维护权,解决保存中主键冲突的问题 @ManyToMany(mappedBy="roles") private Set<SysUser> users = new HashSet<SysUser>(0);
十一 Spring Data JPA中的多表查询
11.1 对象导航查询
对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getLinkMans()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间必须存在关联关系。
查询一个客户,获取该客户下的所有联系人
@Autowired private CustomerDao customerDao; @Test //由于是在java代码中测试,为了解决no session问题,将操作配置到同一个事务中 @Transactional public void testFind() { Customer customer = customerDao.findOne(5l); Set<LinkMan> linkMans = customer.getLinkMans();//对象导航查询 for(LinkMan linkMan : linkMans) { System.out.println(linkMan); } }
查询一个联系人,获取该联系人的所有客户
@Autowired private LinkManDao linkManDao; @Test public void testFind() { LinkMan linkMan = linkManDao.findOne(4l); Customer customer = linkMan.getCustomer(); //对象导航查询 System.out.println(customer); }
对象导航查询的问题分析
问题1:我们查询客户时,要不要把联系人查询出来?
分析:如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的,不使用时又会白白的浪费了服务器内存。
解决:采用延迟加载的思想。通过配置的方式来设定当我们在需要使用时,发起真正的查询。
配置方式:
/** * 在客户对象的@OneToMany注解中添加fetch属性 * FetchType.EAGER :立即加载 * FetchType.LAZY :延迟加载 */ @OneToMany(mappedBy="customer",fetch=FetchType.EAGER) private Set<LinkMan> linkMans = new HashSet<>(0);
问题2:我们查询联系人时,要不要把客户查询出来?
分析:例如:查询联系人详情时,肯定会看看该联系人的所属客户。如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的话,一个对象不会消耗太多的内存。而且多数情况下我们都是要使用的。
解决: 采用立即加载的思想。通过配置的方式来设定,只要查询从表实体,就把主表实体对象同时查出来
配置方式
/** * 在联系人对象的@ManyToOne注解中添加fetch属性 * FetchType.EAGER :立即加载 * FetchType.LAZY :延迟加载 */ @ManyToOne(targetEntity=Customer.class,fetch=FetchType.EAGER) @JoinColumn(name="cst_lkm_id",referencedColumnName="cust_id") private Customer customer;
11.2 使用Specification查询
/** * Specification的多表查询 */ @Test public void testFind() { Specification<LinkMan> spec = new Specification<LinkMan>() { public Predicate toPredicate(Root<LinkMan> root, CriteriaQuery<?> query, CriteriaBuilder cb) { //Join代表链接查询,通过root对象获取 //创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right) //JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接 Join<LinkMan, Customer> join = root.join("customer",JoinType.INNER); return cb.like(join.get("custName").as(String.class),"传智播客1"); } }; List<LinkMan> list = linkManDao.findAll(spec); for (LinkMan linkMan : list) { System.out.println(linkMan); } }