前言

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

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方法,最终将生成如下表关联示意图:

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

接下来通过数据来进行测试,首先我们打开一个会话保存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();
}


Hibernate入门之many to many关系映射详解_多对多_02最终生成的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<>();


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

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

总结

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


你所看到的并非事物本身,而是经过诠释后所赋予的意义