自關聯中@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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM