Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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 25

Three 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.

Thanks for reading!

Donate to the site — we work hard for you.