Spring Data JPA多表联合查询的实例演示
作者:UndefinedMan
对于JPA的基本操作以及动态查询可以查看这篇文章: SpringBoot整合JPA
目前市场上对于ORM框架选型一般只有两种, 一种是Mybatis/MybatisPlus, 另一种就是JPA, 之前参与的项目都是使用MybatisPlus没有系统的了解JPA, 而到新的项目组中使用的是JPA正当学习时发现网络上对于JPA的文章纷繁杂乱, 尤其是对JPA多表联查使我看的摸不到头脑, 所以本着方便自己日后学习以及分享开发经验的出发点, 写下这篇文章.
Mysql版本: Mysql8.0
一. 简单复习数据库表之间的关系
表之间存在哪几种关系?
数据库表之间存在多种关系: 一对一(例子: 一夫一妻), 一对多(例子: 用户与账单), 多对多(例子: 学生与教师)
各个关系如何关联起来?
一对一: 建立外键
一对多: 一对多的关系我们在多的一方放入一(id)来唯一标识
多对多: 多对多的关系通过一张关系表来维持多多关系, 关系表存储了两个表的主键作为外键
综上所述: JPA的多表查询就是基于外键来实现的(实际上JPA并没有对多表查询做封装, 完全使用的是底层Hibernate框架的多表查询实现)
Hibernate对多表联合查询的策略: 通过@OneToOne, @OneToMany, @ManyToOne, @ManyToMany注解自动维护表之间的关联关系, 在CRUD时如果设计到关联表自动建立外键关系对关联表也执行相关操作, 同时提供多个属性支撑懒加载, 过删除等功能.
二. 实现多表联查
在application.peoperties中添加如下配置
server.port=9000
#mysql驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mysql的jdbc连接
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/${your.database}
#用户名
spring.datasource.username=root
#密码
spring.datasource.password=${your.password}
#jpa配置, 显示执行的sql语句
spring.jpa.show-sql=true
#设置hibernate的方言 MySQL5Dialect
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
#自动创建表
spring.jpa.hibernate.ddl-auto=update包结构:

1. 一对一 (OneToOne)
建立两张表consumer(用户表), account(账户表), 我们规定一个用户只能有一个账户, 一个账户只能属于一个用户, 这样便有了一对一的关系, 一对一的关系分为单向一对一, 双向一对一.
首先设计这两个表(因为已经设置了自动创建表所以不用创建数据库表只需要创建实体类映射)
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Integer custId;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_address")
private String custAddress;
}@Entity
@Table(name = "account")
@Data
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "password")
private String password;
}1. 单向一对一
在Consumer中添加对Account的关联
@OneToOne(targetEntity = Account.class,
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
optional = false,
orphanRemoval = false)
@JoinColumn(name = "account_id")
private Account account;这里主要介绍一下@OneToOne()中属性的含义
1. targetEntity: 目标对象, 也就是 "和谁一对一"默认值是注解标注的对象
2. cascade: 设置关联操作, 定义了当主实体做了一些持久化操作时是否级联到关系实体上
有多个可选值 :
| 类型 | 含义 | 示例说明 |
|---|---|---|
| ALL | 级联所有操作 | 相当于 PERSIST + MERGE + REMOVE + REFRESH + DETACH |
| PERSIST | 级联保存 | 当保存主实体时,如果关联实体未被保存,则自动保存 |
| MERGE | 级联合并 | 更新主实体时,也同步更新关联实体的状态(适用于脱离状态的对象) |
| REMOVE | 级联删除 | 删除主实体时,同时删除其关联实体 |
| REFRESH | 级联刷新 | 刷新主实体时,也刷新关联实体的状态(从数据库重新加载数据) |
| DETACH | 级联分离 | 主实体从持久化上下文中移除时,关联实体也一并被分离 |
3. fetch: 定义了是否使用懒加载模式, 默认是即刻加载, 懒加载为LAZY
4. optional: 是否允许标注对象为空, 默认true允许为空
5. orphanRemoval: 是否启用过删除, 如果启用当关系表属性为null时直接删除数据库表中对 应的关联数据
@JoinColumn()定义了生成的外键名
这样就将Customer与Account关联起来了.之后在CRUD时Customer时会自动将Account CRUD前提是定义了允许的关联操作等级.
先进行数据插入:
@SpringBootTest
public class JPAOneToOneTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据1");
Account account = new Account();
account.setName("测试账户1");
customer.setAccount(account);
customerDao.save(customer);
System.out.println("---------保存成功----------");
}
}


数据查询(级联):
@SpringBootTest
public class JPAOneToOneTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据1");
Account account = new Account();
account.setName("测试账户1");
customer.setAccount(account);
customerDao.save(customer);
System.out.println("---------保存成功----------");
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(1).get());
}
}
删除(级联):
@SpringBootTest
public class JPAOneToOneTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据1");
Account account = new Account();
account.setName("测试账户1");
customer.setAccount(account);
customerDao.save(customer);
System.out.println("---------保存成功----------");
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(1).get());
}
@Test
public void test03() {
customerDao.deleteById(1);
System.out.println("----------删除成功----------");
}
}
修改: 对于修改如果开启了过删除, 当设置account为null时会删除掉account表中没有被关联的数据, 注意前提是optinal = true, orphanRemoval = true
@SpringBootTest
public class JPAOneToOneTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据1");
Account account = new Account();
account.setName("测试账户1");
customer.setAccount(account);
customerDao.save(customer);
System.out.println("---------保存成功----------");
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(1).get());
}
@Test
public void test03() {
customerDao.deleteById(1);
System.out.println("----------删除成功----------");
}
@Test
public void test04() {
Customer customer = new Customer();
customer.setCustId(2);
customer.setCustName("测试数据2");
customer.setAccount(null);
customerDao.save(customer);
}
}

2. 双向一对一
如果也在Account中添加@OneToOne() @JoinColumn()这种定义一对一关系就会再次建立一个外键关联Customer, 造成的问题就是互相关联导致两个表中的数据都无法删除, 可以使用@OneToOne(mappedBy = ...)来解决这个问题, mappedBy的意思就是指定这个关联关系的维护者, 简单来说就是总体上只维护一个关联关系.
/**
* mappedBy, 用来标识在一个双向关系中哪一方是关系的维护方
*/
@OneToOne(mappedBy = "account")
private Customer customer;这里只测试查询, 整体与单向一样, 只是方向不同
@SpringBootTest
public class JPAOneToOneAccountTest {
@Autowired
private AccountDao accountDao;
@Test
public void test01() {
System.out.println("Account: " + accountDao.findById(3));
}
}这里输出时会报错:
java.lang.StackOverflowError at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449) at java.lang.StringBuilder.append(StringBuilder.java:142) at com.yangyang.jpa.entity.Customer.toString(Customer.java:16) at java.lang.String.valueOf(String.java:2994) at java.lang.StringBuilder.append(StringBuilder.java:137) at com.yangyang.jpa.entity.Account.toString(Account.java:11) at java.lang.String.valueOf(String.java:2994) at java.lang.StringBuilder.append(StringBuilder.java:137) at com.yangyang.jpa.entity.Customer.toString(Customer.java:16)
可以看出是一个栈溢出, 原因是两个toString循环调用了, 因为输出的前提都是调用各自的toString, 我们重写toString来解决一下
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
", customer=" + customer.getCustName() +
'}';
}
2. 一对多(OneToMany)
建立Message表, 依然自动生成
/**
* 一对多, 多的一方
*/
@Entity
@Table(name = "message")
@Data
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "info")
private String info;
}Customer和Message一对多的关系, 一个用户对应多个消息, 一个消息只属于一个用户.
现在对一对多的关系进行关联, 这里@JoinColumn(name)设置的是放在多的一断一的字段名
@Entity
@Table(name = "customer")
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Integer custId;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_address")
private String custAddress;
// 一对一
@OneToOne(targetEntity = Account.class,
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
optional = true,
orphanRemoval = true)
@JoinColumn(name = "account_id")
private Account account;
// 一对多
@OneToMany(cascade = CascadeType.ALL
, fetch = FetchType.EAGER
, orphanRemoval = false)
@JoinColumn(name = "customer_id")
private List<Message> messages;
}插入(级联):
@SpringBootTest
public class JPAOneToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据3");
List<Message> messages = new ArrayList<>();
messages.add(new Message("haha"));
messages.add(new Message("hehe"));
customer.setMessages(messages);
customerDao.save(customer);
}
}
查询(级联)
@SpringBootTest
public class JPAOneToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据3");
List<Message> messages = new ArrayList<>();
messages.add(new Message("haha"));
messages.add(new Message("hehe"));
customer.setMessages(messages);
customerDao.save(customer);
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(4));
}
}
删除(级联)
@SpringBootTest
public class JPAOneToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据3");
List<Message> messages = new ArrayList<>();
messages.add(new Message("haha"));
messages.add(new Message("hehe"));
customer.setMessages(messages);
customerDao.save(customer);
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(4));
}
@Test
public void test03() {
customerDao.deleteById(4);
System.out.println("----------删除成功----------");
}
}
修改(级联)
@SpringBootTest
public class JPAOneToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
Customer customer = new Customer();
customer.setCustName("测试数据3");
List<Message> messages = new ArrayList<>();
messages.add(new Message("haha"));
messages.add(new Message("hehe"));
customer.setMessages(messages);
customerDao.save(customer);
}
@Test
public void test02() {
System.out.println("Customer: " + customerDao.findById(4));
}
@Test
public void test03() {
customerDao.deleteById(4);
System.out.println("----------删除成功----------");
}
@Test
public void test04() {
Customer customer = new Customer();
customer.setCustId(5);
Message message = new Message();
message.setId(3);
message.setInfo("更改数据1");
Message message1 = new Message();
message1.setId(4);
message1.setInfo("更改数据2");
List<Message> messages = new ArrayList<>();
messages.add(message);
messages.add(message1);
customer.setMessages(messages);
customerDao.save(customer);
}
}
3. 多对一(ManyToOne)
message与customer之间是多对一的关系
定义多对一关系:
@Entity
@Table(name = "message")
@Data
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "info")
private String info;
public Message(String info) {
this.info = info;
}
public Message() {
}
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "customer_id")
private Customer customer;
}以查询(级联)为例, 其他同一对多
@SpringBootTest
public class JPAManyToOneTest {
@Autowired
private MessageDao messageDao;
@Test
public void test01() {
System.out.println(messageDao.findById(3));
}
}

4. 多对多
多对多关系分为单向多对多, 双向多对多
首先还是建立测试表: role, 依然是自动生成
@Entity
@Table(name = "role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "role_name")
private String rName;
}仔细想一下, 其实多对多关系与一对多关系很相似, 问题就在于多对多关系被拆分了, 拆分为两个表对一个中间表的一对多关系, 那么关键就在于定义一个中间关系表, 同时JPA也会自动为我们维护这个关系表无需自己建立.
现在来定义多对多的关系
@Entity
@Table(name = "customer")
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Integer custId;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_address")
private String custAddress;
// 一对一
@OneToOne(targetEntity = Account.class,
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
optional = true,
orphanRemoval = true)
@JoinColumn(name = "account_id")
private Account account;
// 一对多
@OneToMany(cascade = CascadeType.ALL
, fetch = FetchType.EAGER
, orphanRemoval = false)
@JoinColumn(name = "customer_id")
private List<Message> messages;
// 多对多
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(
name = "customer_role_relation", // 中间关系表名
joinColumns = {@JoinColumn(name = "c_id")}, // 当前表的关联外键名
inverseJoinColumns = {@JoinColumn(name = "r_id")} // 另一个多表的关联外键名
)
private List<Role> role;
}着重说一下@JoinTable, 因为需要维护一个关联表, 所以不能使用@JoinColumn, 其中三个参数的意义是name: 关系表表名, joinColumns: 当前表关联外键名, inverseJoinColumns: 另一个表的关联外键名
插入(级联):
@SpringBootTest
public class JPAManyToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
List<Role> rols = new ArrayList<>();
Role role = new Role();
role.setRName("测试role1");
Role role1 = new Role();
role1.setRName("测试role2");
rols.add(role);
rols.add(role1);
Customer customer = new Customer();
customer.setCustName("多对多测试数据1");
customer.setRole(rols);
customerDao.save(customer);
}
}
查询(级联)
@SpringBootTest
public class JPAManyToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
List<Role> rols = new ArrayList<>();
Role role = new Role();
role.setRName("测试role1");
Role role1 = new Role();
role1.setRName("测试role2");
rols.add(role);
rols.add(role1);
Customer customer = new Customer();
customer.setCustName("多对多测试数据1");
customer.setRole(rols);
customerDao.save(customer);
}
@Test
@Transactional
public void test02() {
System.out.println("Customer: " + customerDao.findById(6).get());
}
}
删除(级联):
@SpringBootTest
public class JPAManyToManyTest {
@Autowired
private CustomerDao customerDao;
@Test
public void test01() {
List<Role> rols = new ArrayList<>();
Role role = new Role();
role.setRName("测试role1");
Role role1 = new Role();
role1.setRName("测试role2");
rols.add(role);
rols.add(role1);
Customer customer = new Customer();
customer.setCustName("多对多测试数据1");
customer.setRole(rols);
customerDao.save(customer);
}
@Test
@Transactional
public void test02() {
System.out.println("Customer: " + customerDao.findById(6).get());
}
@Test
public void test03() {
customerDao.deleteById(6);
System.out.println("----------删除成功----------");
}
}
删除这里涉及到一个问题, 如果关联表中存在其他引用关系例如另一个customer也对role有引用, 那么这次这次删除就会失败
修改(级联):
@SpringBootTest
public class JPAManyToManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private RoleDao roleDao;
@Test
public void test01() {
List<Role> rols = new ArrayList<>();
Role role = new Role();
role.setRName("测试role1");
Role role1 = new Role();
role1.setRName("测试role2");
rols.add(role);
rols.add(role1);
Customer customer = new Customer();
customer.setCustName("多对多测试数据1");
customer.setRole(rols);
customerDao.save(customer);
}
@Test
@Transactional
public void test02() {
System.out.println("Customer: " + customerDao.findById(6).get());
}
@Test
public void test03() {
customerDao.deleteById(6);
System.out.println("----------删除成功----------");
}
@Test
@Transactional
@Commit
public void test04() {
// Role role = new Role();
// role.setId(3);
// role.setRName("测试role3");
// Role role1 = new Role();
// role1.setId(4);
// role1.setRName("测试role4");
List<Role> roles = new ArrayList<>();
Role role1 = roleDao.findById(3).get();
roles.add(role1);
Role role2 = roleDao.findById(4).get();
roles.add(role2);
Customer customer = new Customer();
customer.setCustName("多对多测试数据2");
customer.setRole(roles);
customerDao.save(customer);
}
}
这里注意一下, 修改时不可以手动对象id赋值来指定修改哪个字段, 因为在Hibernate中实体有四种状态:
| Transient(瞬时态) | 对象刚 new 出来,尚未与数据库关联,也未被 EntityManager 管理 |
| Persistent(持久态) | 已被 EntityManager 管理,比如通过 find()、merge() 获取或保存的对象 |
| Detached(游离态) | 曾经是持久态,但现在不再被 EntityManager 管理(例如 session 关闭后) |
| Removed(删除态) | 已从数据库中删除 |
我们直接赋值指定的对象被认为是游离态的, 需要是持久态才可以进行更改.简单来说就是在数据库中查出来的才可以证明是存在的才可以进行关联.
补充: 想要关联Role表中已经存在的字段:
/**
* 补充, 想要关联role表中已经存在的字段
*/
@Test
@Transactional
@Commit
public void test05() {
List<Role> rols = new ArrayList<>();
Role role = roleDao.findById(3).get();
rols.add(role);
Role role2 = roleDao.findById(4).get();
rols.add(role2);
Customer customer = new Customer();
customer.setCustName("补充测试数据");
customer.setRole(rols);
customerDao.save(customer);
}

到此这篇关于Spring Data JPA多表联合查询的实例演示的文章就介绍到这了,更多相关Spring Data JPA 多表联合查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
