Многие ко многим
У статьи может быть несколько тегов, а один тег прикреплён ко многим статьям. Ни одна из таблиц не может хранить для этого один внешний ключ, поэтому в базе используют промежуточную таблицу с парами «id статьи — id тега». В Java ничего этого не видно: у статьи есть набор тегов, у тега — набор статей.
Сущность Tag
// src/main/java/com/example/blog/Tag.java
package com.example.blog;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Tag {
@Id
@GeneratedValue
private Long id;
@Column(unique = true, nullable = false)
private String name;
@ManyToMany(mappedBy = "tags")
private Set<Post> posts = new HashSet<>();
protected Tag() {
}
public Tag(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Set<Post> getPosts() {
return posts;
}
}Владеющая сторона в Post
// src/main/java/com/example/blog/Post.java
@ManyToMany
@JoinTable(name = "post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();
public void addTag(Tag tag) {
tags.add(tag);
tag.getPosts().add(this);
}
public void removeTag(Tag tag) {
tags.remove(tag);
tag.getPosts().remove(this);
}@JoinTable задаёт имя промежуточной таблицы и двух её столбцов. Владеть связью может любая сторона; здесь это Post, а Tag ссылается на неё через mappedBy. Используется Set, потому что один тег не должен прикрепляться к статье дважды.
Использование
// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> {
Tag java = new Tag("java");
Tag hibernate = new Tag("hibernate");
em.persist(java);
em.persist(hibernate);
Post first = new Post("Привет, Hibernate");
first.addTag(java);
first.addTag(hibernate);
em.persist(first);
Post second = new Post("Типы record в Java 25");
second.addTag(java);
em.persist(second);
});
List<Post> posts = emf.callInTransaction(em -> em.createQuery(
"select p from Post p join p.tags t where t.name = :name order by p.id", Post.class)
.setParameter("name", "java")
.getResultList());
posts.forEach(p -> IO.println("java: " + p.getTitle()));Hibernate: create table post_tag (post_id bigint not null, tag_id bigint not null, primary key (post_id, tag_id))
Hibernate: insert into Post (title,id) values (?,?)
Hibernate: insert into Post (title,id) values (?,?)
Hibernate: insert into post_tag (post_id,tag_id) values (?,?)
Hibernate: insert into post_tag (post_id,tag_id) values (?,?)
Hibernate: insert into post_tag (post_id,tag_id) values (?,?)
Hibernate: select p1_0.id,p1_0.title from Post p1_0 join post_tag t1_0 on p1_0.id=t1_0.post_id join Tag t1_1 on t1_1.id=t1_0.tag_id where t1_1.name=? order by p1_0.id
java: Привет, Hibernate
java: Типы record в Java 25Три связи — три строки в post_tag. В запросе вы пишете join p.tags t, а Hibernate строит оба соединения через промежуточную таблицу.
Удаление тега
Прежде чем удалить тег, открепите его от всех статей:
// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> {
Tag hibernate = em.createQuery("select t from Tag t where t.name = 'hibernate'", Tag.class)
.getSingleResult();
for (Post post : List.copyOf(hibernate.getPosts())) {
post.removeTag(hibernate);
}
em.remove(hibernate);
});... (три запроса загрузки пропущены)
Hibernate: delete from post_tag where post_id=? and tag_id=?
Hibernate: delete from Tag where id=?List.copyOf делает копию, потому что removeTag меняет тот самый набор, по которому идёт цикл.
Частая ошибка
Если вызвать em.remove(hibernate), не открепив тег, строки в post_tag продолжают на него ссылаться, и база отказывается его удалять:
jakarta.persistence.RollbackException: Error while committing the transaction [could not execute statement [Referential integrity constraint violation: "FK1P3W6FAXSR4INDQPP5SLXJ710: PUBLIC.POST_TAG FOREIGN KEY(TAG_ID) REFERENCES PUBLIC.TAG(ID) (CAST(2 AS BIGINT))"; ...Hibernate ведёт промежуточную таблицу только со стороны владельца — Post.tags. Изменение одного лишь Tag.posts в базе ничего не меняет.