SpringData JPA中@OneToMany和@ManyToOne的用法詳解
一. 假設需求場景
在我們開發的過程中,經常出現兩個對象存在一對多或多對一的關系。如何在程序在表明這兩個對象的關系,以及如何利用這種關系優雅地使用它們。
其實,在javax.persistence包下有這樣兩個註解——@OneTomany和@ManyToOne,可以為我們所用。
現在,我們假設需要開發一個校園管理系統,管理各大高校的學生。這是一種典型的一對多場景,學校和學生的關系。這裡,我們涉及簡單的級聯保存,查詢,刪除。
二. 代碼實現
2.1 級聯存儲操作
Student類和School類
@Data @Table @Entity @Accessors(chain = true) public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; @ManyToOne @JoinColumn(name = "school_fk") private School school; }
Student類上面的四個註解不做解釋,id主鍵使用自增策略。Student中有個School的實例變量school,表明學生所屬的學校。@ManyToOne(多對一註解)代表在學生和學校關系中“多”的那方,學生是“多”的那方,所以在Student類裡面使用@ManyToOne。
那麼,@ManyToOne中One當然是指學校瞭,也就是School類。
@JoinColumn(name = “school_fk”)指明School類的主鍵id在student表中的字段名,如果此註解不存在,生成的student表如下:
@Data @Table @Entity @Accessors(chain = true) public class School { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; @OneToMany(mappedBy="school",cascade = CascadeType.PERSIST) private List<Student> students; }
在School類中,維護一個類型為List的students實例變量。@OneToMany(一對多註解)代表在學生和學校關系中“一”的那方,學校是“一”的那方,所以在School類裡面使用@OneToMany。
那麼,@OneToMany中many當然是指學生瞭,也就是Student類。註意@OneToMany中有個mappedBy參數設置為school,這個值是我們在Student類中的School類型的變量名;cascade參數表示級聯操作的類型,它隻能是CascadeType的6種枚舉類型。
有的博客經常寫成cascade = CascadeType.ALL,這其實會誤導大傢,因為裡面的級聯刪除會讓你懷疑人生。
我們先使用CascadeType.PERSIST,表示在持久化的級聯操作,也就是保存學校的時候可以一起保存學生。
StudentRepository和SchoolRepository
public interface StudentRepository extends JpaRepository<Student, Integer> { } public interface SchoolRepository extends JpaRepository<School, Integer> { }
測試類
@RunWith(SpringRunner.class) @SpringBootTest public class MultiDateSourceApplicationTests { @Autowired SchoolRepository schoolRepository; @Test public void contextLoads() { Student jackMa = new Student().setName("Jack Ma"); Student jackChen = new Student().setName("Jack Chen"); School school = new School().setName("湖畔大學"); List<Student> students = new ArrayList<>(); students.add(jackMa); students.add(jackChen); jackMa.setSchool(school); jackChen.setSchool(school); school.setStudents(students); schoolRepository.save(school); } }
運行測試類後,數據庫的表數據如下:
在程序中,我們並沒有調用StudentRepository的save方法,但是我們在@OneToMany中添加瞭級聯保存參數CascadeType.PERSIST,所以在保存學校的時候能自動保存學生, jackMa.setSchool(school);jackChen.setSchool(school);這兩句肯定不能少的。
2.2 查詢操作和toSting問題
上面的添加操作成功瞭,讓我們來試試查詢操作。
控制臺:打印出的錯誤是org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.cauchy6317.multidatesource.cascadestudy.entity.School.students, could not initialize proxy – no Session
這是因為@OneToMany的fetch參數默認設置為FetchType.Lazy模式,即懶加載模式。
也就是說,我們查詢mySchool的時候,並沒有把在該學校的學生查出來。而且,School類的toString方法需要知道students,所以debug模式下mySchool變量報錯。
我們把@OneToMany的fetch參數改為Fetch.EAGER,即熱加載。
@OneToMany(mappedBy="school", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) private List<Student> students;
再運行一次…
這次的錯誤是StackOverflowError,為什麼會這樣呢?堆棧溢出,也就是我們寫的程序出現瞭死循環。可是我們都沒寫循環語句啊,不急,我們先看看這個mySchool數據。
我們發現mySchool裡面有students,而且students裡面又有school變量,變量school裡面自然又有students瞭。由此看來,是這個死循環的導致。也就是Student和School的toString方法,循環調用彼此。
所以隻需要修改其中一個的toString方法,使它的toString方法不涉及另一個類型的變量,也就是排除另一個類型的變量。lombok考慮到這點瞭,可以使用ToString.exclude。
在官網的ToString介紹頁面中,我看到瞭這個有意思的小字部分。
哈哈哈,這個地方已經說明瞭如果使用數組中包含自身,ToString方法會報StackOverflowError。
那麼,我們在Student類中使用ToString.exclude,還是在School類中使用ToString.exclude呢?我們先在School類中試試。
@ToString.Exclude @OneToMany(mappedBy="school", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) private List<Student> students;
這次我們把學生也打印出來一個。
可以看到,mySchool的ToString方法沒有將students打印出來;student的toSting方法將School打印出來瞭。如果在Student類的school變量上使用@ToString.EXCLUDE的話,那麼mySchool就會打印出很多student來。
所以,我覺得還是在private List students;上使用@ToString.EXCLUDE較好。
2.3 級聯刪除
前面我們說過級聯刪除會讓人懷疑人生,讓我們用代碼來感受一下。
@ToString.Exclude @OneToMany(mappedBy="school", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER) private List<Student> students;
我們在School類中,使用級聯刪除。也就是說,當我們刪除某個學校的時候,把這個學校下的所有學生刪除掉!
現在查看數據庫的表,可以清楚的看到。school中id為1的學校沒有瞭,而且student中學校外鍵為1的學生也全部被刪瞭。或許你會覺得這也沒什麼大不瞭的,因為學校不存在瞭,學校裡的學生自然不存在瞭。好,那就讓我們來見識一下級聯刪除的真正威力。我們如果也在Student類中使用瞭級聯刪除會怎麼樣?
@ManyToOne(cascade = CascadeType.REMOVE) @JoinColumn(name = "school_fk") private School school;
也就是說,當我們刪除某個學生時,會級聯刪除學生所在的學校。我們用代碼測試一下是不是這樣。
public interface StudentRepository extends JpaRepository<Student, Integer> { /** * 根據姓名刪除學生對象 * @param name * @return */ @Transactional Integer deleteByName(String name); }
可以看到數據插入成功瞭,當我們放掉斷點後。
可以看到出現瞭三條刪除語句,我再看看數據庫的學生表,發現Jack Chen也被刪除瞭。這是因為我們在Student類和School類中都使用瞭級聯刪除,當我們刪除Jack Ma的時候,級聯刪除瞭湖畔大學,當刪除湖畔大學後又級聯刪除瞭所有湖畔大學的student。這就好比,你打算開除一個學生,結果把學校和學生的數據全刪沒瞭。是不是很刺激?
2.4 pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.28</version> </dependency> </dependencies>
環境:springboot2.1.7+jdk1.8+mysql8.0+druid1.1.10+Springdata JPA+Lombok
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- cascade級聯關系操作案例詳解
- jpa實體@ManyToOne @OneToMany無限遞歸方式
- 基於Jpa中ManyToMany和OneToMany的雙向控制
- Java Hibernate中的持久化類和實體類關系
- jpa onetomany 使用級連表刪除被維護表數據時的坑