Hibernate入门之many to many关系映射详解

前言

上一节我们讲解完了一对多映射,本节我们进入到关系映射最后一节即多对多关系映射,文中若有错误之处,还望指正。

many to many关系映射 

本节我们所给出的实体是post和tag,发表一篇博客文章对应可以选择多个标签,而一个标签下也可以对应多篇发表的文章,这是典型的多对多关系,所以二者关系配置如下:

@Entity
public class Post {

    public Post() {
    }

    public Post(String title) {
        this.title = title;
    }

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "post_tag",
            joinColumns = @JoinColumn(name = "post_id"),
            inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private List<Tag> tags = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void addTag(Tag tag) {
        tags.add(tag);
        tag.getPosts().add(this);
    }

    public void removeTag(Tag tag) {
        tags.remove(tag);
        tag.getPosts().remove(this);
    }
}
@Entity
public class Tag {
    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @ManyToMany(mappedBy = "tags")
    private List<Post> posts = new ArrayList<>();

    public Tag() {
    }

    public Tag(String name) {
        this.name = name;
    }

    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Tag tag = (Tag) o;
        return Objects.equals(name, tag.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

多对多对关系通过生成第三张表即中间表来进行关联,同时我们也知道tag属于post,所以通过属性mappedBy来进行关联,tag实体具有唯一的业务键即name属性,该键用特定于Hibernate的@NaturalId注解,我们可通过唯一业务键来判断tag是否相等,所以我们重写了equals和hashCode方法,最终将生成如下表关联示意图:

接下来通过数据来进行测试,首先我们打开一个会话保存post和tag,然后再打开一个会话将已保存返回的某一个post的主键进行查询,最终实例化一个tag,通过查询出来的post中的tag集合移除实例化的tag,我们看看最终生成的SQL语句是否如我们所期望的那样:

        Transaction transaction = null;

        Post post1 = new Post("JPA with Hibernate");
        Post post2 = new Post("Native Hibernate");

        Tag tag1 = new Tag("Java");
        Tag tag2 = new Tag("Hibernate");

        post1.addTag(tag1);
        post1.addTag(tag2);

        post2.addTag(tag1);

        try (Session session = HibernateUtil.getSessionFactory().openSession()) {

            transaction = session.beginTransaction();

            session.save(post1);
            session.save(post2);

            transaction.commit();

        } catch (Exception e) {
            if (transaction != null) {
                transaction.rollback();
            }
            e.printStackTrace();
        }

        try (Session session = HibernateUtil.getSessionFactory().openSession()) {

            transaction = session.beginTransaction();

            Tag newTag = new Tag("Java");

            Post querypost1 = session.find(Post.class, post1.getId());

            querypost1.removeTag(newTag);

            transaction.commit();

        } catch (Exception e) {
            if (transaction != null) {
                transaction.rollback();
            }
            e.printStackTrace();
        }

最终生成的SQL语句如上,我们看到基于给定的post_id,然后将post_tag表中的所对应的post_id都已经删除,这点完全没毛病,但是最终又重新插入了一条,很显然,因为我们所实例化的tag处于未被Hibernate跟踪的状态,所以才有了先删除,然后再执行重新插入操作,在实际情况下我们可以认为这样操作没有什么很大意义,我们只是想移除在post_tag表中post_id所对应的数据,从数据库层面来看,这样操作毫无效率可言,因为数据库要做更多额外的工作,如重建索引。执行上述插入操作的问题出在对于目标实体所使用的集合类型,我们应该使用Set<>类型而不是List<>,接下来我们将实体post和tag中所对应的实体集合改造成如下:

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "post_tag",
            joinColumns = @JoinColumn(name = "post_id"),
            inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private Set<Tag> tags = new HashSet<>();
    @ManyToMany(mappedBy = "tags")
    private Set<Post> posts = new HashSet<>();

此时我们看到生成的SQL语句仅执行一条DELETE语句,该语句将删除关联的post_tag表数据,而没有了重新插入操作,完美解决问题。

总结

本节我们重点讲解了在Hibernate中的多对多关系映射要使用对目标实体和源实体集合要使用Set<>集合类型而非List<>集合类型,否则将可能会执行多余而不必要的操作,下一节我们开始进入到Hibernate中实体状态的详细讲解。

posted @ 2020-03-08 13:53  Jeffcky  阅读(819)  评论(0编辑  收藏  举报