前言
上一节我们讲解完了一对多映射,本节我们进入到关系映射最后一节即多对多关系映射,文中若有错误之处,还望指正。
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中实体状态的详细讲解。