JPA中的一对多
相关注解说明
@OneToMany
: 建立一对多的关系映射targetEntityClass
: 指定多的一方的类的字节码mappedBy
: 指定从表实体类中引用主表对象的名称。cascade
: 指定要使用的级联操作fetch
: 指定是否采用延迟加载orphanRemoval
:是否使用孤儿删除
@ManyToOne
: 建立多对一的关系映射targetEntityClass
:指定一的一方实体类字节码cascade
:指定要使用的级联操作fetch
:指定是否采用延迟加载optional
:关联是否可选。如果设置为false,则必须始终存在非空关系。
@JoinColumn
: 用于定义主键字段和外键字段的对应关系name
:指定外键字段的名称referencedColumnName
:指定引用主表的主键字段名称unique
:是否唯一。默认值不唯一nullable
:是否允许为空。默认值允许。insertable
:是否允许插入。默认值允许。updatable
:是否允许更新。默认值允许。columnDefinition
:列的定义信息。
编写实体类
案例中使用的是学生和班级的关系,班级和学生是一对多关系,即一个班级可以有多个学生。
Student.java
@Setter
@Getter
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
@ManyToOne(targetEntity = ClassInfo.class) // 配置学生和班级是多对一关系
@JoinColumn(name = "class_id",referencedColumnName = "id") // 配置外键
private ClassInfo classInfo;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
需要注意的是,如果使用lombok 则不能使用@Data
注解,只要@Setter
、@Getter
然后重写toString()方法不需要打印关联属性。,否则会在打印控制台时保存报错
ClassInfo.java
@Setter
@Getter
@Entity
@Table(name = "class")
public class ClassInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String className;
@OneToMany(mappedBy = "classInfo") // 放弃外键维护权
private Set<Student> students = new HashSet<>();
@Override
public String toString() {
return "ClassInfo{" +
"id=" + id +
", className='" + className + '\'' +
'}';
}
}
关于放弃维护权:
- 如果ClassInfo不放弃维护权,在插入数据时就会发送一条多余的update语句,浪费资源。
- 通常在一对多关系中,多的一方放弃维护权
Dao接口层省略...
一对多操作
所有操作建议都事务中操作,减少不必要的错误
添加操作
需求:同时保存一个班级和一个学生,并建立他们之间的关联关系
@Test
@Transactional
@Rollback(false)
public void test1() {
Student student = new Student();
student.setName("张三");
student.setAge(18);
ClassInfo classInfo = new ClassInfo();
classInfo.setClassName("1001班");
// 最少也要在外键维护的一方配置关联关系,或者双方都配置
student.setClassInfo(classInfo);
classInfoDao.save(classInfo);
studentDao.save(student);
}
注意:单独配置放弃外键维护权的一方设置关联关系不起作用
删除操作
- 如果主表(班级表)的数据又被外键表(学生表)引用,要删除班级表的数据必须先把学生表的数据删除。
- 删除外键表(学生表)的数据可以随时删除。
级联操作
指操作一个对象同时操作它的关联对象
cascade:配置级联操作
- CascadeType.MERGE 级联更新
- CascadeType.PERSIST 级联保存:
- CascadeType.REFRESH 级联刷新:
- CascadeType.REMOVE 级联删除:
- CascadeType.ALL 包含所有
级联通常需要在双方都配置
配置方式:
- 班级实体类部分代码:
@OneToMany(mappedBy = "classInfo",,cascade = CascadeType.ALL) // 放弃外键维护权
private Set<Student> students = new HashSet<>();
- 学生实体类部分代码:
@ManyToOne(targetEntity = ClassInfo.class,cascade = CascadeType.ALL) // 配置学生和班级是多对一关系
@JoinColumn(name = "class_id",referencedColumnName = "id") // 配置外键
private ClassInfo classInfo;
级联添加
通过保存外键维护权的一方实现级联添加
@Test
public void test2() {
Student student = new Student();
student.setName("李四");
student.setAge(18);
ClassInfo classInfo = new ClassInfo();
classInfo.setClassName("1002班");
// 设置单向关联即可
student.setClassInfo(classInfo);
// 保存外键维护放
studentDao.save(student);
}
如果需要保存放弃维护权的一方来实现级联添加,则双方均需要配置关联关系
@Test
public void test3() {
Student student = new Student();
student.setName("王五");
student.setAge(18);
ClassInfo classInfo = new ClassInfo();
classInfo.setClassName("1002班");
// 配置双方的关联关系
classInfo.getStudents().add(student);
student.setClassInfo(classInfo);
// 保存放弃维护权的一方
classInfoDao.save(classInfo);
}
级联删除
@Test
public void test4() {
classInfoDao.deleteById(5L);
}
如果删除班级,则在该班级的所有学生都会被删除,级联删除要谨慎使用!
多对多操作
相关注解说明
-
@ManyToMany : 用于映射多对多关系
- cascade:配置级联操作。
- fetch:配置是否采用延迟加载。
- targetEntity:配置目标的实体类。映射多对多的时候不用写。
-
@JoinTable :针对中间表的配置
- name:配置中间表的名称
- joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
- inverseJoinColumn:中间表的外键字段关联对方表的主键字段
-
@JoinColumn : 用于定义主键字段和外键字段的对应关系。
- name:指定外键字段的名称
- referencedColumnName:指定引用主表的主键字段名称
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
编写实体类
该案例使用用户和角色的关系,一个用户可以有多个角色,一个角色也可以有多个用户。是多对多关系。
User.java
@Setter
@Getter
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userName;
private Integer age;
/*
* 配置用户到角色的多对多关系
* */
@ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL) // 声明表关系
@JoinTable(
// 中间表名
name = "user_role",
// 当前对象在中间表的外键
joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "id"),
// 对方对象在中间表的外键
inverseJoinColumns = @JoinColumn(name = "role_id",referencedColumnName = "id")
) // 配置中间表
private Set<Role> roles = new HashSet<>();
@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
Role.java
@Setter
@Getter
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roleName;
/*
* 配置角色到用户的多对多关系
* */
@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL) // 放弃维护中间表
private Set<User> users = new HashSet<>();
@Override
public String toString() {
return "Role{" +
"id=" + id +
", roleName='" + roleName + '\'' +
'}';
}
关于多对多关系的放弃维护权:
- 推荐在被动的一方放弃维护权,用户和角色关系中,角色属于被选择的所以配置它放弃维护权。
多对多操作
- 和一对多操作没什么区别
- 需要记住的就是如果没有配置级联添加,在同时添加用户和角色时,需要先保存角色(即放弃维护权的一方先保存),在事务中无所谓
对象导航查询
对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。
@Test
@Transactional // 为了解决no session问题
public void test8() {
// 根据用户查询所有角色
User user = userDao.findById(2L).get();
System.out.println(user);
Set<Role> roles = user.getRoles();
roles.forEach(System.out::println);
}
@Test
@Transactional
public void test9() {
// 根据角色查询所有用户
Role role = roleDao.findById(2L).get();
System.out.println(role);
Set<User> users = role.getUsers();
users.forEach(System.out::println);
}
细节:
- 在对象导航查询中,从“一”查询到“多"默认使用延迟加载。从多方查询一方默认使用立即加载
关于放弃维护权再次总结:
- 添加关联关系时最少要在维护外键(中间表)的一方配置关联关系,或者双方设置关联关系。
- 在没有配置级联关系且没有事务的情况下,同时在主表和外键表的同时添加数据时需要先保存放弃维护权的一方。否则会报错
object references an unsaved transient instance
- 在一对多关系中,通常多的一方放弃维护权
- 在多对多关系中,通常被动的一方放弃维护权
- 建议都在事务中操作,减少不必要的错误