自关联中@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": [
            
          ]
        }
      ]
    }
  ]
}
View Code
// 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": [
      
    ]
  }
]
View Code

经过以上处理之后,我们很容易得到一个类似二叉树的递归结构:

  • 根据每一条数据库纪录进行递归查找,起点是自己,直到最后一级子区域;
  • 在多层级且层级数未知的情况,要用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=?
View Code

四、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字段解决。

参考链接

https://www.cnblogs.com/ealenxie/p/9800818.html

https://blog.csdn.net/qq_22327273/article/details/88578187

posted @ 2020-06-10 21:41  又是火星人  阅读(2649)  评论(0编辑  收藏  举报