JPA中的OneToMany和ManyToOne的最佳实践
概述
我们在使用JPA的时候,会使用OneToMany表示对象之间的一对多的关系,ManyToOne表示多对一的关系。多对一和一对多只是对象关系中的正面描述和反面描述,在JPA中不一样的定义方法会有不同的SQL生成,对性能会有比较大的影响。比如我们举一个例子:一个学校会有很多的学生,很多学生都会在一个学校上学,那么我们应该会设计一个父对象School和一个子对象Student,School实体会和和Student之间有一对多的关联关系,Student实体和School实体有多对一的关联关系。在JPA中对于上述的场景描述我们会有三个不同的做法:
- 在父对象中使用@OneToMany建立和子对象的关系关系
- 在父对象中使用@OneToMany和@JoinColumn来建立和子对象的关系
- 建立双向关系,在父对象中使用OneToMany,在子对象中使用ManyToOne
- 在子对象中使用ManyToOne建立和父对象的联系
在父对象中使用@OneToMany建立和子对象的关系关系
单向关系是只我们只在School实体建立和Student的关系,不在Student中建立和School的关联关系,具体看如下的代码:
import javax.persistence.*;
import java.util.Set;
@Entity
@Setter(AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GenericGenerator(name = "native", strategy = "native")
@Column(name = "id", updatable = false, nullable = false)
private long id;
private String name;
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Student> students;
}
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Setter
@Getter
@EqualsAndHashCode
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GenericGenerator(name = "native", strategy = "native")
private long id;
private String name;
}
基于如上的实体定义,然后我们运行如下的测试类
import com.wangzhenhua.jpa.entity.School;
import com.wangzhenhua.jpa.entity.Student;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
public class PersistenceTest {
private static SessionFactory factory;
@BeforeClass
public static void setup() {
Configuration configuration = new Configuration();
factory = configuration.configure().buildSessionFactory();
}
@Test
public void saveSchool() {
School school = new School();
school.setName("demo");
Set<Student> studentSet = new HashSet<Student>();
Student student1 = new Student();
student1.setName("s1");
studentSet.add(student1);
Student student2 = new Student();
student2.setName("s2");
studentSet.add(student2);
Student student3 = new Student();
student3.setName("s3");
studentSet.add(student3);
school.setStudents(studentSet);
Session session = factory.openSession();
Transaction tx = session.beginTransaction();
session.persist(school);
tx.commit();
session.close();
}
}
会有如下的SQL生成:
五月 26, 2019 9:54:11 下午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
Hibernate: insert into School (name) values (?)
Hibernate: insert into Student (name) values (?)
Hibernate: insert into Student (name) values (?)
Hibernate: insert into Student (name) values (?)
Hibernate: insert into School_Student (School_id, students_id) values (?, ?)
Hibernate: insert into School_Student (School_id, students_id) values (?, ?)
Hibernate: insert into School_Student (School_id, students_id) values (?, ?)
从生成的日志中,我们可以发现有一个中间表School_Student表,这是多对多的关系中才应该生成的表,而我们目前是一对多的场景,可以看出使用单向的OneToMany标注的时候,JPA并不会生成符合我们的预期表,这里其实在 Student表中建立一个School表的外键就可以,因此最后的三个更新多对多表的SQL也没必要存在。 在父对象中使用@OneToMany和@JoinColumn来建立和子对象的关系
import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.List;
import java.util.Set;
@Entity
@Setter(AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class School {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
@JoinColumn(name = "school_id")
private List<Student> students;
}
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Setter
@Getter
@EqualsAndHashCode
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
}
如上的实体定义会生成如下的sql语句:
Hibernate: insert into School (name, id) values (?, ?)
Hibernate: insert into Student (name, id) values (?, ?)
Hibernate: insert into Student (name, id) values (?, ?)
Hibernate: insert into Student (name, id) values (?, ?)
Hibernate: update Student set school_id=? where id=?
Hibernate: update Student set school_id=? where id=?
Hibernate: update Student set school_id=? where id=?
可以看出这里没有生成多对多的中间表,而是在student表中多了一个school_id的字段,要比上面生成的sql要精简了很多,但是问题是这里创建student的school_id的值是通过update语句更新进去的。因此这还不是一个最佳的sql语句。 建立双向关系,在父对象中使用OneToMany,在子对象中使用ManyToOne
所谓双向关系,就是在School实体中利用OneToMany关系建立和Student实体的关系,然后再在Student实体中利用ManyToOne标注建立和School的实体关系,具体看如下的代码:
import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Set;
@Entity
@Setter(AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GenericGenerator(name = "native", strategy = "native")
@Column(name = "id", updatable = false, nullable = false)
private long id;
private String name;
@OneToMany(
mappedBy = "school",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Student> students;
}
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Setter
@Getter
@EqualsAndHashCode
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@ManyToOne
@JoinColumn(name = "school_id")
private School school;
}
运行上面的单元测试后,生成如下的SQL
Hibernate: insert into School (name) values (?)
Hibernate: insert into Student (name, school_id) values (?, ?)
Hibernate: insert into Student (name, school_id) values (?, ?)
Hibernate: insert into Student (name, school_id) values (?, ?)
我们可以看出这个结果是我们预期生成的,并没有额外的SQL产生。但是这里还存在的问题是在School中定义的OneToMany有导致内存泄漏的风险,因为我们没有办法控制它获取数据的数量,即使我们使用了懒加载,也有内存泄漏的问题。
在子对象中使用ManyToOne建立和父对象的联系
因为我们使用OneToMany有内存溢出的风险,因为JPA没有提供可以控制获取记录数量的参数,因此OneToMany只适合在关联的子对象实体不多的场景里,在其他场景里,为了避免内存溢出的风险,我们最好只用ManyToOne来建立对象之间的关系,具体做法如下:
import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.List;
import java.util.Set;
@Entity
@Setter(AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class School {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
}
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Setter
@Getter
@EqualsAndHashCode
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@ManyToOne
@JoinColumn(name = "school_id")
private School school;
}
其实ManyToOne满足我们的所有一对多关系的所有新增、查询的场景,比如增加实体,可以通过如下的代码:
@Test
public void saveSchool() {
School school = new School();
school.setName("demo");
List<Student> studentSet = new ArrayList<Student>();
Student student1 = new Student();
student1.setName("s1");
student1.setSchool(school);
Student student2 = new Student();
student2.setName("s2");
student2.setSchool(school);
Student student3 = new Student();
student3.setName("s3");
student3.setSchool(school);
studentSet.add(student1);
studentSet.add(student2);
studentSet.add(student3);
Session session = factory.openSession();
Transaction tx = session.beginTransaction();
session.persist(school);
session.persist(student1);
session.persist(student2);
session.persist(student3);
tx.commit();
session.close();
}
会生成如下的sql 语句:
Hibernate: insert into School (name, id) values (?, ?)
Hibernate: insert into Student (name, school_id, id) values (?, ?, ?)
Hibernate: insert into Student (name, school_id, id) values (?, ?, ?)
Hibernate: insert into Student (name, school_id, id) values (?, ?, ?)
可以看出这里输出的sql是符合我们的预期,并没有额外的sql生成。
如果我们要查询一个School的所有学生,我们可以利用如下的HQL语句查询:
session = factory.openSession();
Query<Student> query = session.createQuery("select s from Student s where s.school.id = :schoolId", Student.class);
query.setParameter("schoolId",1l);
List<Student> resultList = query.getResultList();
session.close();
会生成如下的sql语句:
Hibernate: select student0_.id as id1_1_, student0_.name as name2_1_, student0_.school_id as school_i3_1_ from Student student0_ where student0_.school_id=?
Hibernate: select school0_.id as id1_0_0_, school0_.name as name2_0_0_ from School school0_ where school0_.id=?
因此ManyToOne可以满足在一对多场景里的所有需求,因此推荐使用ManyToOne来作为一对多的关联管理。