Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Многие ко многим

У статьи может быть несколько тегов, а один тег прикреплён ко многим статьям. Ни одна из таблиц не может хранить для этого один внешний ключ, поэтому в базе используют промежуточную таблицу с парами «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 в базе ничего не меняет.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.