本课展示了如何创建一对多的关系。

在这一课中,我们将创建一个双向的一对多的关系,一个Player可以有很多Registration

让我们给这个模型添加一些现实生活中的约束。

球员和注册之间的双向关联意味着,如果我们有一个Player对象,我们就可以得到所有的Registration对象,反之亦然,我们可以通过使用注册得到一个球员。在上一节课讨论的单向一对多关系中,我们可以在给定的Tournament中找到Registration对象,但我们无法从Registration对象中找到Tournament

对/tournaments和/registrations的GET请求

对/tournaments和/registrations的GET请求

一对多关系的反向是多对一,即许多的注册信息映射到一个玩家。

截屏2022-05-27 20.28.06.png

  1. 对于双向关系的例子,在onetomany包内创建一个名为bi的新包。将onetomany.uni包中的RegistrationTournament类以及onetoone包中的PlayerPlayerProfile类以及相关的数据接入层、服务层和控制器类复制到onetomany.bi包。

  2. 我们将从更新Registration类开始。为了有一个双向的关系,我们将在上一课定义的注册类中添加一个Player字段。为新字段生成gettersetter方法,并更新构造函数和toString方法。

    @Entity
    public class Registration {
        @Id
        @GeneratedValue(strategy=GenerationType.SEQUENCE)
        private int id;
        private Date registrationDate;
    
        private Player player;
        //getters and setters
        //constructor
        //toString method
    }
    

@ManyToOne

  1. Registration类和Player类之间存在着多对一的关系,许多注册信息可以映射到一个球员。多对一双向关系的多方总是关系的所有方。

    为了模拟这种关系,我们将使用@ManyToOne注解,并在@JoinColumn中指定与注册表的外键列相对应的列。球员表有一列id,它将成为注册表中的外键列player_id。这就是注册表如何知道如何找到它的球员。

    @ManyToOne
    @JoinColumn(name="player_id", referencedColumnName = "id")
    private Player player;
    

    @JoinColumn注解也显示了这是关系的所有方。

    截屏2022-05-27 20.32.19.png

级联类型

  1. 接下来,我们将为这种关系选择级联类型。如果一个Registration对象被删除,相关的Player不应该被删除。这意味着,删除操作不应该被级联。由于我们对级联类型有细微的控制,我们将列出所有的级联类型,除了REMOVE

    @ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE, 
                        CascadeType.DETACH,  CascadeType.REFRESH}
    @JoinColumn(name="player_id", referencedColumnName = "id")
    private Player player;
    
  2. 现在我们将更新Player类,以显示锦标赛的注册情况。由于一个球员可以有多个注册信息,我们将添加一个注册列表作为该类的一个新字段。

    private List<Registration> registrations = new ArrayList<>(); 
    //generate getter and setter methods
    

@OneToMany

  1. Player类与Registration类有一对多的关系,因为一个球员可以注册很多场比赛。这可以通过@OneToMany注解来模拟。

    由于多方(Registration)是双向关系的拥有方,我们将在这里使用mappedBy属性(在Player类中)来指定这是关系的逆向方。

    @OneToMany(mappedBy="player", cascade= CascadeType.ALL)
    private List<Registration> registrations = new ArrayList<>();
    

    我们在这里使用级联类型的ALL,因为我们希望在删除球员记录时删除球员的注册信息。

    mappedBy属性中的球员引用了注册类中的Player字段。Hibernate通过查看Player字段上的@JoinColumn注解来找到外键列。

    截屏2022-05-27 20.43.00.png

    接下来,我们将为Player类添加一个方法,设置双向关系。在这个方法中,我们将向Player添加一个Registration对象,同时更新Registration以反映它属于这个Player

    //与注册类建立双向关系
    public void registerPlayer(Registration reg) {
        //将注册添加到列表中
        registrations.add(reg);
        //在注册中设置Player字段
        reg.setPlayer(this);
    }
    

    PlayerController类中,我们将添加一个新的PUT映射,将一个带有registration_id的注册和一个以id为键的Player联系起来,如下所示。

    @PutMapping("/{id}/registrations/{registration_id}")
    public Player assignRegistration(@PathVariable int id, @PathVariable int registration_id) {
       Registration registration = registrationService.getRegistration(registration_id); 
       return service.assignRegistration(id, registration);
    }
    

    控制器类的方法调用服务类的方法assignRegistration,输入PlayeridRegistration对象。PlayerService类中的方法显示。

    public Player assignRegistration(int id, Registration registration) {
        Player player = repo.findById(id).get();
        player.registerPlayer(registration);
        return repo.save(player);
    }
    

    在设置了PlayerRegistration实体之间的双向关系后,我们数据库的更新ERD如下所示。同样的情况可以从H2数据库的web控制台中得到验证(在http://localhost:8080/h2-console,连接URL为jdbc:h2:mem:testdb)。

    截屏2022-05-27 20.48.32.png