一、一對一關系
擁有端:
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Person { /** * 關系的擁有端存儲一個被控端的一個外鍵。 * 在這個例子中 Person表 中的 address_id 就是指向 address表 的一個外鍵, * 缺省情況下這個外鍵的字段名稱,是以它指向的表的名稱加下划線“_”加“id”組成的。 * 當然我們也可以根據我們的喜好來修改這個字段,修改的辦法就是使用 @JoinColumn 這個注解。 * 在這個例子中我們可以將這個注解標注在 Person 類中的 Address 屬性上去。 */ @Id private Long id; private String firstName; private String lastName; @OneToOne private Address address; }
被控端:
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Address { /** * mappedBy = (Optional) The field that owns the relationship,即指向擁有端的(變量名). */ @Id private Long id; private String state; private String city; private String street; private String zipCode; @OneToOne(mappedBy = "address") private Person person; }
表結構:
二、一對多關系
擁有端:
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Comment { /** * 一對多關系中,一般都是選擇“多”這端作為擁有端,因為可以很方便把“一”這端作為一個屬性包含進來。 */ @Id private Integer id; private Integer year; private boolean approved; private String content; @ManyToOne private Post post; }
被控端:
@Entity @Data @AllArgsConstructor @NoArgsConstructor public class Post { /** * 一對多的被控端,往往是“一”這端,需要以List的方式將“多”端添加進來 */ @Id private Integer id; private String title; private String content; @OneToMany(mappedBy = "post") private List<Comment> comments; }
表結構:
三、自關聯
事實上,在國內互聯網領域很少使用外鍵,database也不會交給ORM管理,table結構會保持一定程度的字段冗余。個人不太習慣用JPA管理映射關系,思維和經驗都沒有轉變過來,但是在多級分類的表結構(省市區表、商品分類表)當中,往往是以自關聯的方式組織樹形結構數據的,不需要建立外鍵也可以發揮JPA的優勢。
@Entity @Table @Data public class Area { @Id @GeneratedValue private Long id; // 區域名 private String name; // 父區域 @ManyToOne(fetch = FetchType.LAZY) // 相當於把2個Area寫在一處; @JsonIgnore // 忽略父類屬性JSON序列化; private Area parent; // 子區域,一個區域信息可以有多級子區域,比如 : 廣東省 - 廣州市 - 天河區 @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) private List<Area> children; }
Repository接口:
public interface AreaRepository extends JpaRepository<Area, Long> {}
利用測試方法把數據寫入數據庫:
@Autowired private AreaRepository areaRepository; @Test public void addArea() { // 廣東省 (頂級區域) Area guangdong = new Area(); guangdong.setName("廣東省"); areaRepository.save(guangdong); //廣東省 下面的 廣州市(二級區域) Area guangzhou = new Area(); guangzhou.setName("廣州市"); guangzhou.setParent(guangdong); areaRepository.save(guangzhou); //廣州市 下面的 天河區(三級區域) Area tianhe = new Area(); tianhe.setName("天河區"); tianhe.setParent(guangzhou); areaRepository.save(tianhe); //廣東省 下面的 湛江市(二級區域) Area zhanjiang = new Area(); zhanjiang.setName("湛江市"); zhanjiang.setParent(guangdong); areaRepository.save(zhanjiang); //湛江市 下面的 霞山區(三級區域) Area xiashan = new Area(); xiashan.setName("霞山區"); xiashan.setParent(zhanjiang); areaRepository.save(xiashan); }
最后我們可以得到如下的表結構:
id name parent_id 1 廣東省 null 2 廣州市 1 3 天河區 2 4 湛江市 1 5 霞山區 4
添加一個最簡單的RESTful接口,主要是實現JSON序列化查看結果,
@RestController public class OutputController { @Autowired private AreaRepository areaRepository; @GetMapping("area") public Area getArea() { List<Area> areas = areaRepository.findAll(); return areas.get(0); } @GetMapping("areas") public List<Area> getAreas() { return areaRepository.findAll(); } }
返回結果:

// 20200611004952 // http://localhost:8080/area { "id": 1, "name": "廣東省", "children": [ { "id": 2, "name": "廣州市", "children": [ { "id": 3, "name": "天河區", "children": [ ] } ] }, { "id": 4, "name": "湛江市", "children": [ { "id": 5, "name": "霞山區", "children": [ ] } ] } ] }

// 20200611005129 // http://localhost:8080/areas [ { "id": 1, "name": "廣東省", "children": [ { "id": 2, "name": "廣州市", "children": [ { "id": 3, "name": "天河區", "children": [ ] } ] }, { "id": 4, "name": "湛江市", "children": [ { "id": 5, "name": "霞山區", "children": [ ] } ] } ] }, { "id": 2, "name": "廣州市", "children": [ { "id": 3, "name": "天河區", "children": [ ] } ] }, { "id": 3, "name": "天河區", "children": [ ] }, { "id": 4, "name": "湛江市", "children": [ { "id": 5, "name": "霞山區", "children": [ ] } ] }, { "id": 5, "name": "霞山區", "children": [ ] } ]
經過以上處理之后,我們很容易得到一個類似二叉樹的遞歸結構:
- 根據每一條數據庫紀錄進行遞歸查找,起點是自己,直到最后一級子區域;
- 在多層級且層級數未知的情況,要用SQL語句獲得類似結果,還蠻考驗思維和SQL基礎的;
- 存在ORM的“N+1”問題

Hibernate: /* select generatedAlias0 from Area as generatedAlias0 */ select area0_.id as id1_0_, area0_.name as name2_0_, area0_.parent_id as parent_i3_0_ from area area0_ Hibernate: select children0_.parent_id as parent_i3_0_0_, children0_.id as id1_0_0_, children0_.id as id1_0_1_, children0_.name as name2_0_1_, children0_.parent_id as parent_i3_0_1_ from area children0_ where children0_.parent_id=? Hibernate: select children0_.parent_id as parent_i3_0_0_, children0_.id as id1_0_0_, children0_.id as id1_0_1_, children0_.name as name2_0_1_, children0_.parent_id as parent_i3_0_1_ from area children0_ where children0_.parent_id=? Hibernate: select children0_.parent_id as parent_i3_0_0_, children0_.id as id1_0_0_, children0_.id as id1_0_1_, children0_.name as name2_0_1_, children0_.parent_id as parent_i3_0_1_ from area children0_ where children0_.parent_id=? Hibernate: select children0_.parent_id as parent_i3_0_0_, children0_.id as id1_0_0_, children0_.id as id1_0_1_, children0_.name as name2_0_1_, children0_.parent_id as parent_i3_0_1_ from area children0_ where children0_.parent_id=? Hibernate: select children0_.parent_id as parent_i3_0_0_, children0_.id as id1_0_0_, children0_.id as id1_0_1_, children0_.name as name2_0_1_, children0_.parent_id as parent_i3_0_1_ from area children0_ where children0_.parent_id=?
四、ORM框架的“N+1”問題
下面兩篇文章很好的解釋了ORM的“N+1”問題:
- JPA:https://www.cnblogs.com/google4y/p/3455534.html
- Django:https://www.the5fire.com/what-is-orm-n+1.html
在解決N+1問題上,Django比JPA要方便很多,在ORM語法和查詢靈活度上,感覺也是Django更勝一籌。后續,我們將利用@NamedEntityGraph來解決“N+1”問題。
五、注意事項:
- 關於@OntToMany等一對多映射屬性,如果想要用Set<T>代替List<T>時,請在類上添加注解@EqualsAndHashCode(exclude = "children"),否則序列化時會因為@Data注解自動生成的equals和hashCode方法而發生Could not write JSON: Infinite recursion錯誤。
- 如果在sout打印Area對象時,發生java.lang.StackOverflowError錯誤(堆棧溢出),可以用@ToString.Exclude排除掉children字段解決。
參考鏈接