Many-to-many
A post can have several tags, and one tag is attached to many posts. Neither table can hold a single foreign key for this, so the database uses a join table with pairs "post id — tag id". In Java you see none of it: a post has a set of tags, a tag has a set of posts.
The Tag entity
// 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;
}
}The owning side in 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 names the join table and its two columns. Either side can own the relationship; here it is Post, and Tag refers to it with mappedBy. A Set is used because a tag should not be attached to the same post twice.
Using it
// 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("Hello, Hibernate");
first.addTag(java);
first.addTag(hibernate);
em.persist(first);
Post second = new Post("Records in 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: Hello, Hibernate
java: Records in Java 25Three links — three rows in post_tag. In the query you write join p.tags t and Hibernate builds both joins through the link table.
Removing a tag
Before deleting a tag, detach it from all posts:
// 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);
});... (three loading queries are skipped)
Hibernate: delete from post_tag where post_id=? and tag_id=?
Hibernate: delete from Tag where id=?List.copyOf makes a copy, because removeTag changes the set we iterate over.
Common mistake
If you call em.remove(hibernate) without detaching the tag, rows in post_tag still refer to it, and the database refuses to delete it:
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 maintains the join table only from the owning side — Post.tags. Changing just Tag.posts does not change the database.