引言

在关系数据库系统中,通过外键列来链接两个表之间的一对多关联,以便子表记录引用父表的主键。

Many To One

尽管在关系型数据库管理系统(RDBMS)中可能很简单,但在涉及 JPA 时,一对多数据库关联可以通过 @ManyToOne@OneToMany 关联来表示,因为面向对象编程(OOP)的关联可以是单向的,也可以是双向的。

@ManyToOne 注解允许您在子实体映射中映射外键列,以便子实体具有对其父实体的实体对象引用。这是映射数据库一对多数据库关联的最自然方式,通常也是最有效的替代方法。

为方便起见,为了利用实体状态转换和脏检查机制,许多开发人员选择将子实体映射为父对象中的集合,为此,JPA 提供 @OneToMany 注解。

很多时候,最好用查询替换集合,这在查询性能方面要灵活得多。但是,有时候映射集合是正确的事情,然后你有两个选择:

  • 单向 @OneToMany 关联
  • 双向 @OneToMany 关联

双向关联要求子实体映射提供 @ManyToOne 注解,该注解负责控制关联

另一方面,单向 @OneToMany 关联更简单,因为它只是在父端定义关系。在本文中,我将解释 @OneToMany 关联的问题,以及如何克服它们。

映射 @OneToMany 关联的方法有很多种。我们可以使用List或Set。 我们也可以定义@JoinColumn注释。 那么,让我们看看所有这些是如何工作的。

单向 @OneToMany

考虑我们有以下映射:

@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @OneToMany(
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();
 
    //Constructors, getters and setters removed for brevity
}
 
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String review;
 
    //Constructors, getters and setters removed for brevity
}

现在,如果我们持久化1个 Post 和3个 PostComment:

Post post = new Post("First post");
 
post.getComments().add(
    new PostComment("My first review")
);
post.getComments().add(
    new PostComment("My second review")
);
post.getComments().add(
    new PostComment("My third review")
);
 
entityManager.persist(post);

Hibernate 将执行以下 SQL 语句:

insert into post (title, id) 
values ('First post', 1)
 
insert into post_comment (review, id) 
values ('My first review', 2) 
 
insert into post_comment (review, id) 
values ('My second review', 3)
 
insert into post_comment (review, id) 
values ('My third review', 4)
 
insert into post_post_comment (Post_id, comments_id) 
values (1, 2)
 
insert into post_post_comment (Post_id, comments_id) 
values (1, 3)
 
insert into post_post_comment (Post_id, comments_id) 
values (1, 4)

那是什么!为什么执行这么多查询?无论如何,post_post_comment 表的处理是什么?

默认情况下,这就是单向 @OneToMany 关联的工作方式,这是从数据库角度看它的样子:

One To Many

对于数据库管理员(DBA)来说,这看起来更像是多对多数据库关联而不是一对多关系,并且它也不是很有效。 我们现在有三个表,而不是两个表,所以我们使用的存储空间超过了必要的数量。本来应该只有一个外键,我们现在有两个。我们最有可能将这些外键编入索引,因此我们需要两倍的内存来缓存此关联的索引。不太好!

使用 @JoinColumn 的单向 @OneToMany

要解决上述额外的连接表问题,我们只需要添加 @JoinColumn

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "post_id")
private List<PostComment> comments = new ArrayList<>();

@JoinColumn 注解帮助 Hibernate(最着名的 JPA 提供者)找出 post_comment 表中一个 post_id 外键列来定义这种关联。

有了这个注释,当持久化三个 PostComment 实体时,我们得到以下 SQL 输出:

insert into post (title, id) 
values ('First post', 1)
 
insert into post_comment (review, id) 
values ('My first review', 2)
 
insert into post_comment (review, id) 
values ('My second review', 3)
 
insert into post_comment (review, id) 
values ('My third review', 4)
 
update post_comment set post_id = 1 where id = 2
 
update post_comment set post_id = 1 where id =  3
 
update post_comment set post_id = 1 where id =  4

好一点,但这三个更新语句的目的是什么?

如果你看一下 Hibernate flush order,你会发现在处理集合元素之前执行了持久化操作。 这样,Hibernate 首先插入子记录而不使用外键,因为子实体不存储此信息。 在集合处理阶段,外键列会相应更新。

相同的逻辑适用于集合状态修改,因此从子集合中删除第一项时:

post.getComments().remove(0);

Hibernate 执行两个语句而不是一个:

update post_comment set post_id = null where post_id = 1 and id = 2
 
delete from post_comment where id=2

同样,首先执行父实体状态更改,这将触发子实体更新。之后,当处理集​​合时,orphan removal 操作将执行子行删除语句。

双向 @OneToMany

映射 @OneToMany 关联的最佳方法是依赖 @ManyToOne 端传播所有实体状态更改:

@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @OneToMany(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();
 
    //Constructors, getters and setters removed for brevity
 
    public void addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }
 
    public void removeComment(PostComment comment) {
        comments.remove(comment);
        comment.setPost(null);
    }
}
 
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String review;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
 
    //Constructors, getters and setters removed for brevity
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PostComment )) return false;
        return id != null && id.equals(((PostComment) o).id);
    }
    @Override
    public int hashCode() {
        return 31;
    }
}

关于上述映射有几点需要注意:

  • @ManyToOne 关联使用 FetchType.LAZY,否则,我们将使用 EAGER 提取,这对性能有害
  • 父实体 Post 有两个实用方法(例如 addComment 和 removeComment),用于同步双向关联的两端。 无论何时使用双向关联,都应始终提供这些方法,否则,您将面临非常微妙的状态传播问题
  • 子实体 PostComment 实现了 equals 和 hashCode 方法。 由于我们不能依赖自然标识符进行相等性检查,因此我们需要使用实体标识符。但是,您需要正确执行此操作,以使所有实体状态转换中的相等性保持一致。因为我们依赖于 removeComment 的相等性,所以在双向关联中覆盖子实体的 equals 和 hashCode 是一种好习惯。

如果我们持久化3个 PostComment:

Post post = new Post("First post");
 
post.addComment(
    new PostComment("My first review")
);
post.addComment(
    new PostComment("My second review")
);
post.addComment(
    new PostComment("My third review")
);
 
entityManager.persist(post);

Hibernate 为每个持久化的 PostComment 实体生成一个 SQL 语句:

insert into post (title, id) 
values ('First post', 1)
 
insert into post_comment (post_id, review, id) 
values (1, 'My first review', 2)
 
insert into post_comment (post_id, review, id) 
values (1, 'My second review', 3)
 
insert into post_comment (post_id, review, id) 
values (1, 'My third review', 4)

如果我们移除一个 PostComment:

Post post = entityManager.find( Post.class, 1L );
PostComment comment1 = post.getComments().get( 0 );
 
post.removeComment(comment1);

只有一个删除 SQL 语句被执行:

delete from post_comment where id = 2

因此,双向 @OneToMany 关联是在我们真正需要在父端映射一对多关系集合时的最佳方式。

只有 @ManyToOne

虽然你可以有选择的使用 @OneToMany 注解,但这并不意味着它应该是每个一对多数据库关系的默认选项。 集合的问题在于,当子记录的数量相当有限时,我们才使用它们。

因此,实际上,@OneToMany 只有当 Many(多) 意味着 Few(一些) 时才是实用的。也许 @OneToFew 会成为这个注解的更具启发性的名称。

正如我在此 StackOverflow回答 中所解释的那样,您无法限制 `@OneToMany 集合的大小,就像您使用查询级别分页一样。

因此,大多数情况下,子项上的 @ManyToOne 注解就是您需要的一切。 但是,如何获得与 Post 实体关联的子实体?

好吧,你只需要一个JPQL查询:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.post.id = :postId", PostComment.class)
.setParameter( "postId", 1L )
.getResultList();

这会转换为简单的 SQL 查询:

select pc.id AS id1_1_,
       pc.post_id AS post_id3_1_,
       pc.review AS review2_1_
from   post_comment pc
where  pc.post_id = 1

即使不再管理集合,在必要时添加/删除子实体也相当简单。 至于更新子对象,即使您不使用托管集合,脏检查机制也可以正常工作。 使用查询的好处在于,您可以按照自己喜欢的方式对其进行分页,这样,如果子实体的数量随时间增长,则应用程序性能不会受到影响。

结论

正如您将在以后的文章中看到的那样,双向集合比单向集合更好,因为它们依赖于 @ManyToOne 关联,它在生成的 SQL 语句方面始终是高效的。

但是,即使它们非常方便,您也不必总是使用集合。 @ManyToOne 关联是映射一对多数据库关系的最自然且最有效的方式。

原文链接