Jpa 中怎么玩一對(duì)多?
Jpa 中的一對(duì)一、一對(duì)多沒(méi)搞明白的話,總會(huì)覺(jué)得有點(diǎn)繞,今天咱們來(lái)簡(jiǎn)單聊聊這個(gè)話題。
1. 一對(duì)一
比如說(shuō)一個(gè)學(xué)校有一個(gè)地址,一個(gè)地址只有一個(gè)學(xué)校。
那么我們可以按照如下方式來(lái)設(shè)計(jì)類:
@Data
@Entity
@Table(name = "t_address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer aid;
private String province;
private String city;
private String area;
private String phone;
@OneToOne(cascade = CascadeType.ALL)
private School school;
}
@Data
@Entity
@Table(name = "t_school")
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer sid;
private String name;
@OneToOne(cascade = CascadeType.ALL)
private Address address;
}
一對(duì)一的關(guān)系,可以只在 School 中維護(hù),也可以只在 Address 中維護(hù),也可以兩者都維護(hù),具體哪種,那就看需求了。
在上面的例子中,我們?cè)?School 和 Address 中都通過(guò) @OneToOne 注解來(lái)維護(hù)了一對(duì)一的關(guān)系。
cascade 用來(lái)配置級(jí)聯(lián)操作,有如下取值:
- ALL:所有操作
- PERSIST:級(jí)聯(lián)添加
- MERGE:級(jí)聯(lián)更新
- REMOVE:級(jí)聯(lián)刪除
- REFRESH:級(jí)聯(lián)刷新
根據(jù)自己需求選擇合適的就行。
這樣,最終創(chuàng)建出來(lái)的 t_school 表和 t_address 表中,會(huì)分別多出來(lái)一個(gè)字段 address_aid 和 school_sid,這兩個(gè)字段都是外鍵,正是通過(guò)外鍵,將兩張表中不同的記錄關(guān)聯(lián)起來(lái)。
有的人可能不習(xí)慣這種自動(dòng)添加的字段,那也可以自定義該字段,反正該字段總是要有的,自定義的方式如下:
@Data
@Entity
@Table(name = "t_address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer aid;
private String province;
private String city;
private String area;
private String phone;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "sid",referencedColumnName = "sid")
private School school;
@Column(insertable = false,updatable = false)
private Integer sid;
}
@Data
@Entity
@Table(name = "t_school")
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer sid;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "aid",referencedColumnName = "aid")
private Address address;
@Column(insertable = false,updatable = false)
private Integer aid;
}
在 Address 中自定義一個(gè) sid,并設(shè)置該字段不可添加和修改,然后通過(guò) @JoinColumn 注解去指定關(guān)聯(lián)關(guān)系,@JoinColumn 注解中的 name 表示的是當(dāng)前類中的屬性名,referencedColumnName 表示的則是 School 類中對(duì)應(yīng)的屬性名。
在 School 類中做相似的操作。
最后啟動(dòng)項(xiàng)目去觀察 MySQL 中生成的表。
2. 一對(duì)多
一個(gè)班級(jí)中有多個(gè)學(xué)生,而一個(gè)學(xué)生只屬于一個(gè)班級(jí),我們可以這樣來(lái)定義實(shí)體類:
@Data
@Table(name = "t_student")
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer sid;
private String name;
@ManyToOne(cascade = CascadeType.ALL)
private Clazz clazz;
}
@Data
@Table(name = "t_clazz")
@Entity
public class Clazz {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer cid;
private String name;
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private List<Student> students;
}
Student 和 Clazz 的關(guān)系是多對(duì)一,用 @ManyToOne 注解,Clazz 和 Student 的關(guān)系是一對(duì)多,用 @OneToMany 注解。
Student 和 Clazz 的關(guān)系是多對(duì)一,將來(lái)的 t_student 表中會(huì)多出來(lái)一個(gè)屬性 clazz_cid,通過(guò)這個(gè)外鍵將 Student 和 Clazz 關(guān)聯(lián)起來(lái)。如果我們不想要自動(dòng)生成的 clazz_cid,那么也可以自定義,方式如下:
@Data
@Table(name = "t_student")
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer sid;
private String name;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "cid")
private Clazz clazz;
@Column(insertable = false,updatable = false)
private Integer cid;
}
定義一個(gè) cid 屬性,并設(shè)置為不可編輯和不可添加,然后通過(guò) @JoinColumn 注解配置 cid 屬性為外鍵。
Clazz 和 Student 的關(guān)系是一對(duì)多,這個(gè)是通過(guò)一個(gè)自動(dòng)生成的第三張表來(lái)實(shí)現(xiàn)的,如下:
3. 測(cè)試
3.1 添加測(cè)試
先來(lái)個(gè)一對(duì)一的添加測(cè)試,如下:
public interface SchoolRepository extends JpaRepository<School,Integer> {
}
@SpringBootTest
class JpaOneToManyApplicationTests {
@Autowired
SchoolRepository schoolRepository;
@Test
void contextLoads() {
School school = new School();
school.setSid(1);
school.setName("哈佛大學(xué)");
Address address = new Address();
address.setAid(1);
address.setProvince("黑龍江");
address.setCity("哈爾濱");
address.setArea("某地");
address.setPhone("123456");
school.setAddress(address);
schoolRepository.save(school);
}
}
在這個(gè)測(cè)試過(guò)程中,關(guān)聯(lián)關(guān)系是由 t_school 一方來(lái)維護(hù)了,因此將來(lái)填充的外鍵是 t_school 中的 aid。添加結(jié)果如下圖:
t_school
t_address
這是一個(gè)簡(jiǎn)單的添加案例。
更新也是調(diào)用 save 方法,更新的時(shí)候會(huì)先判斷這個(gè) id 是否存在,存在的話就更新,不存在就添加。
再來(lái)看班級(jí)的添加,如下:
public interface ClazzRepository extends JpaRepository<Clazz,Integer> {
}
@Autowired
ClazzRepository clazzRepository;
@Test
void test02() {
Clazz c = new Clazz();
c.setCid(1);
c.setName("三年級(jí)二班");
List<Student> students = new ArrayList<>();
Student s1 = new Student();
s1.setSid(1);
s1.setName("javaboy");
students.add(s1);
Student s2 = new Student();
s2.setSid(2);
s2.setName("張三");
students.add(s2);
c.setStudents(students);
clazzRepository.save(c);
}
注意,添加的是班級(jí),所以班級(jí)和學(xué)生之間關(guān)系就由第三張表來(lái)維護(hù),而不是由學(xué)生來(lái)維護(hù)。
3.2 查詢測(cè)試
再來(lái)一個(gè)簡(jiǎn)單的查詢,假設(shè)我們現(xiàn)在想根據(jù)省份來(lái)搜索學(xué)校,如下:
public interface SchoolRepository extends JpaRepository<School,Integer> {
List<School> findSchoolByAddressProvince(String province);
}
@Autowired
SchoolRepository schoolRepository;
@Test
void test01() {
List<School> list = schoolRepository.findSchoolByAddressProvince("黑龍江");
System.out.println("list = " + list);
}
松哥給大家捋一下 Spring Data 如何解析上面自定義的查詢方法:
- 首先截取掉 findSchoolByAddressProvince 的前綴,剩下 AddressProvince。
- 檢查 School 是否有 addressProvince 屬性,有就按照該屬性查詢,對(duì)于我們的案例,并沒(méi)有 addressProvince 屬性,所以繼續(xù)下一步。
- 從右側(cè)駝峰開(kāi)始拆分,拆掉第一個(gè)駝峰后面的內(nèi)容,我們這里拆分之后只剩下 Address 了,判斷 School 是否存在 Address 屬性,不存在就繼續(xù)重復(fù)該步驟,繼續(xù)切掉右側(cè)第一個(gè)駝峰。
- 在上文案例中,School 中有 address 屬性,所以接下來(lái)就去檢查 address 中是否有 province 屬性,因?yàn)槲覀冞@里只剩下一個(gè) province 了,如果剩下的字符串類似于 provinceAaaBbb 這種,那么繼續(xù)按照第三步去解析。
上面這個(gè)寫(xiě)法有一個(gè)小小的風(fēng)險(xiǎn),假設(shè) School 中剛好就有一個(gè)屬性叫做 addressProvince,那么此時(shí)的分析就會(huì)出錯(cuò)。所以,對(duì)于上面的查詢,我們也可以定義成如下方式:
public interface SchoolRepository extends JpaRepository<School,Integer> {
List<School> findSchoolByAddress_Province(String province);
}
此時(shí)就不會(huì)產(chǎn)生歧義了,系統(tǒng)就知道 province 是 address 的屬性了。
再來(lái)一個(gè)班級(jí)的查詢,如下:
public interface ClazzRepository extends JpaRepository<Clazz,Integer> {
}
@Test
void test03() {
List<Clazz> list = clazzRepository.findAll();
System.out.println("list = " + list);
}
如果在查詢的過(guò)程中,需要對(duì)學(xué)生進(jìn)行排序,可以添加如下屬性:
@Data
@Table(name = "t_clazz")
@Entity
public class Clazz {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer cid;
private String name;
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
@OrderBy("sid desc")
private List<Student> students;
}
通過(guò) @OrderBy("sid desc") 可以設(shè)置查詢的 student 排序。