自关联中@ManyToOne、@OneToMany的使用
一、一对一关系
拥有端:
@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字段解决。
参考链接