JPA 多表操作
表关系
- 一对一
- 一对多
- 一的一方:主表
- 多的一方:从表
- 外键:需要在从表上新建一列作为外键,它的取值来源于主表的主键。
- 多对多:
- 中间表:中间表最少应该有两个字段组成,这两个字段作为外键指向两张表的主键,又组成了联合主键。
案例:客户(一方)和联系人(多方)
映射的注解说明(一对多)
@OneToMany:
作用:建立一对多的关系映射
属性:
- targetEntityClass:指定多的一方的类的字节码
- mappedBy:指定从表实体类中引用主表对象的名称。
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- orphanRemoval:是否使用孤儿删除
@ManyToOne
作用:建立多对一的关系
属性:
- targetEntityClass:指定一的一方实体类字节码
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- optional:关联是否可选。如果设置为false,则必须始终存在非空关系。
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
- name:指定外键字段的名称
- referencedColumnName:指定引用主表的主键字段名称
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
客户:
@Entity//表示当前类是一个实体类
@Table(name="cst_customer")//建立当前实体类和表之间的对应关系
public class Customer implements Serializable {
@Id//表明当前私有属性是主键
@GeneratedValue(strategy=GenerationType.IDENTITY)//指定主键的生成策略
@Column(name="cust_id")//指定和数据库表中的cust_id列对应
private Long custId;
@Column(name="cust_name")//指定和数据库表中的cust_name列对应
private String custName;
@Column(name="cust_source")//指定和数据库表中的cust_source列对应
private String custSource;
@Column(name="cust_industry")//指定和数据库表中的cust_industry列对应
private String custIndustry;
@Column(name="cust_level")//指定和数据库表中的cust_level列对应
private String custLevel;
@Column(name="cust_address")//指定和数据库表中的cust_address列对应
private String custAddress;
@Column(name="cust_phone")//指定和数据库表中的cust_phone列对应
private String custPhone;
//配置客户和联系人的一对多关系
//使用注解形式配置多表关系:
// 1.声明关系
// @OneToMany:配置一对多关系
// targetEntity:对方对象的字节码对象
// 2.配置外键(多对多:中间表)
// @JoinColumn:配置外键
// name:外键字段名称(从表中)
// referencedColumnName:参照的主表的主键字段名称
//------- 在客户实体类上(一的一方)添加了外键配置,所以对客户而言,具有了维护外键的作用-----------
@OneToMany(targetEntity=LinkMan.class)
@JoinColumn(name="lkm_cust_id",referencedColumnName="cust_id")
private Set<LinkMan> linkmans = new HashSet<LinkMan>(0);
//++++++++++++++省略GetSet以及构造方法++++++++++++++++++++++++
联系人:
@Entity
@Table(name="cst_linkman")
public class LinkMan implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="lkm_id")
private Long lkmId;
@Column(name="lkm_name")
private String lkmName;
@Column(name="lkm_gender")
private String lkmGender;
@Column(name="lkm_phone")
private String lkmPhone;
@Column(name="lkm_mobile")
private String lkmMobile;
@Column(name="lkm_email")
private String lkmEmail;
@Column(name="lkm_position")
private String lkmPosition;
@Column(name="lkm_memo")
private String lkmMemo;
//配置联系人和客户的一对多关系
//使用注解形式配置多表关系:
// 1.声明关系
// @ManyToOne:配置多对一关系
// targetEntity:对方对象的字节码对象
// 2.配置外键(多对多:中间表)
// @JoinColumn:配置外键
// name:外键字段名称(从表中)
// referencedColumnName:参照的主表的主键字段名称
//------- 在联系人实体类上(多的一方)添加了外键配置,所以对联系人而言,也具有了维护外键的作用-------
@ManyToOne(targetEntity=Customer.class)
@JoinColumn(name="lkm_cust_id",referencedColumnName="cust_id")
private Customer customer;
//++++++++++++++省略GetSet以及构造方法++++++++++++++++++++++++
注:在客户和联系人中都配置了外键,可以通过联系人找到客户,也可通过客户找到联系人的关系。称这种为双向关系。
一对多操作
保存
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class OneToManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/**
* 保存操作
* 需求:
* 保存一个客户和一个联系人
* 要求:
* 创建一个客户对象和一个联系人对象
* 建立客户和联系人之间关联关系(双向一对多的关联关系)
* 先保存客户,再保存联系人
* 问题:
* 当我们建立了双向的关联关系之后,先保存主表,再保存从表时:
* 会产生2条insert(分别向两张表中写入相应数据)和1条update(更新外键:一的一方通过主键向多的一方表中的外键字段写入数据).
*/
@Test
@Transactional //开启事务
@Rollback(false)//设置为不回滚
public void testAdd() {
Customer c = new Customer();
c.setCustName("TBD云集中心");
c.setCustLevel("VIP客户");
c.setCustSource("网络");
c.setCustIndustry("商业办公");
c.setCustAddress("昌平区北七家镇");
c.setCustPhone("010-84389340");
LinkMan l = new LinkMan();
l.setLkmName("TBD联系人");
l.setLkmGender("male");
l.setLkmMobile("13811111111");
l.setLkmPhone("010-34785348");
l.setLkmEmail("98354834@qq.com");
l.setLkmPosition("老师");
l.setLkmMemo("还行吧");
//-------体现实体之间的关联关系,不然完成添加操作后,从表外键字段中会是一个空值-----------
c.getLinkMans().add(l);//配置一到多的关联关系(两表保存数据后,发送一条update语句给外键赋值)
//l.setCustomer(c);//配置多到一的关联关系(当保存时,就已经对外键进行赋值)
//----------------------------------------------------------------------
linkManDao.save(l);
customerDao.save(c);
}
}
运行上述代码可以发现:会产生2条insert和1条update语句(由于一的一方会维护外键,发送update语句),而实际开发中我们只需要2条insert。解决此问题,只需要在一的一方放弃维护权
放弃一的一方维护权只要对一的一方进行如下配置即可:
//@OneToMany(targetEntity=LinkMan.class)
//@JoinColumn(name="lkm_cust_id",referencedColumnName="cust_id")
//private Set<LinkMan> linkmans = new HashSet<LinkMan>(0);
//放弃外键进行如下配置即可,只声明一对多的关系,不做多余的如上述外键配置
@OneToMany(mappedBy="customer")
//++++++++mappedBy的作用就是声明一对多关系的映射就是依靠多的一方实体中的customer属性的映射关系。+++++++
private Set<LinkMan> linkmans = new HashSet<LinkMan>(0);
删除
@Autowired
private CustomerDao customerDao;
@Test
@Transactional
@Rollback(false)//设置为不回滚
public void testDelete() {
customerDao.delete(1l);
}
删除操作的说明如下:
删除从表数据:可以随时任意删除。
删除主表数据:
有从表数据:
1、在默认情况下,它会把外键字段更新为null,然后删除主表数据。如果在数据库的从表表结构上,外键字段有非空约束,默认情况就会报错了。
2、如果主表配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null没有关系)因为它根本没有更新外键字段的权利,如果还想删除,使用级联删除引用。
- 没有从表数据:随便删,所以可以先删除从表数据再删除主表数据。
在实际开发中,级联删除请慎用!(在一对多的情况下)
多表操作中踩的坑:
在多表操作中一般不要用Lombok,否则可能导致死循环栈溢出
一对多级联操作
指操作一个对象同时操作它的关联对象,如:级联添加(更新),级联删除
对于级联操作,只需要在映射属性上配置级联关系cascade
/**
* cascade:配置级联操作
* CascadeType.MERGE 级联更新
* CascadeType.PERSIST 级联保存:
* CascadeType.REFRESH 级联刷新:
* CascadeType.REMOVE 级联删除:
* CascadeType.ALL 包含所有
*/
@OneToMany(mappedBy="customer",cascade=CascadeType.ALL)
通过上述配置就可以对客户实体进行级联操作了
@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 testJIlianAdd() {
Customer c = new Customer();
c.setCustName("TBD云集中心");
LinkMan l = new LinkMan();
l.setLkmName("TBD联系人");
c.getLinkMans().add(l);
l.setCustomer(c);
//级联添加:保存客户的同时保存他的所有联系人
customerDao.save(c);
}
@Test
@Transactional //开启事务
@Rollback(false)//设置为不回滚
public void testJIlianDel() {
//级联删除:删除ID为1客户的同时删除他的所有联系人
customerDao.delete(1);
}
}
多对多操作
案例:用户和角色
映射的注解说明(多对多)
@ManyToMany
作用:用于映射多对多关系
属性:
- cascade:配置级联操作。
- fetch:配置是否采用延迟加载。
- targetEntity:配置目标的实体类。映射多对多的时候不用写。
@JoinTable
作用:针对中间表的配置
属性:
- name:配置中间表的名称
- <font color='cornflowerblue'>joinColumns:中间表的外键字段关联`当前实体类`所对应表的主键字段 </font>
- <font color='cornflowerblue'>inverseJoinColumn:中间表的外键字段关联`对方实体类表`的主键字段</font>
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
- name:指定外键字段的名称
- referencedColumnName:指定引用主表的主键字段名称
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
用户实体:
@Entity
@Table(name="sys_user")
public class SysUser implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="user_id")
private Long userId;
@Column(name="user_code")
private String userCode;
@Column(name="user_name")
private String userName;
@Column(name="user_password")
private String userPassword;
@Column(name="user_state")
private String userState;
//多对多关系映射
@ManyToMany(mappedBy="users")//mappedBy放弃维护权,因为如果两个实体都进行了中间表的配置,在进行
//添加操作时会报错,因为两个实体都具有中间表的维护权,都想向中间表中插入数据,所以报错,主键重复。中间表 //中两个外键字段的值的插入,由一个实体通过多对多映射关系完成即可。
private Set<SysRole> roles = new HashSet<SysRole>(0);
//++++++++++++++省略GetSet以及构造方法++++++++++++++++++++++++
角色实体:
@Entity
@Table(name="sys_role")
public class SysRole implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="role_id")
private Long roleId;
@Column(name="role_name")
private String roleName;
@Column(name="role_memo")
private String roleMemo;
//多对多关系映射
@ManyToMany(targetEntity=SysRole.class)
@JoinTable(name="user_role_rel",//中间表的名称
//中间表user_role_rel字段关联sys_role表的主键字段role_id
joinColumns={@JoinColumn(name="role_id",referencedColumnName="role_id")},
//中间表user_role_rel的字段关联sys_user表的主键user_id
inverseJoinColumns={@JoinColumn(name="user_id",referencedColumnName="user_id")}
)
private Set<SysUser> users = new HashSet<SysUser>(0);
//++++++++++++++省略GetSet以及构造方法++++++++++++++++++++++++
🤣级联添加操作与一对多同理
保存
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/**
* 问题:
* 在保存时,会出现主键重复的错误,因为都是要往中间表中保存数据造成的。
* 解决办法:
* 让任意一方放弃维护关联关系的权利
*/
@Test
@Transactional //开启事务
@Rollback(false)//设置为不回滚
public void test1(){
//创建对象
SysUser u1 = new SysUser();
u1.setUserName("用户1");
SysRole r1 = new SysRole();
r1.setRoleName("角色1");
SysRole r2 = new SysRole();
r2.setRoleName("角色2");
//建立关联关系
u1.getRoles().add(r1);
u1.getRoles().add(r2);
r2.getUsers().add(u1);
r1.getUsers().add(u1);
//保存
roleDao.save(r1);
roleDao.save(r2);
userDao.save(u1);
}
删除
@Autowired
private UserDao userDao;
/**
* 删除操作
* 在多对多的删除时,双向级联删除根本不能配置
* 禁用
* 如果配了的话,如果数据之间有相互引用关系,可能会清空所有数据
*/
@Test
@Transactional
@Rollback(false)//设置为不回滚
public void testDelete() {
userDao.delete(1l);
}
多表查询
对象导航查询
查询一个对象的时候,通过此对象一并查询出它的关联对象(前提是设置了关联关系)
如上述的客户与联系人的例子:
查询一个客户,获取该客户下的所有联系人
@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);
@Autowired
private CustomerDao customerDao;
@Test
@Transactional
public void testFind() {
//当使用延迟加载(默认)的时候
//调用get方法获取关联对象时,并不会立即发起查询,而是在使用关联对象时才会查询
Customer customer = customerDao.findOne(5l);//仅仅发起查询客户相关属性的SQL语句
Set<LinkMan> linkMans = customer.getLinkMans();//如果是延迟加载,调用get时不会立即把所有 //联系人查出,而是在如下面语句要使用linkMans时,在发起查询,查询所有的联系人信息。但如果是立即加载,在调用 //get时,就已经把所有联系查出了。
System.out.println(linkMans.size());
}
- Post link: http://sovzn.github.io/2021/07/15/%E5%A4%9A%E8%A1%A8%E6%93%8D%E4%BD%9C/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues