Hibernate: How to map ManyToMany association with external table and composite key?
Этот интересный вопрос возник передо мной, когда на новом проекте разработка началась по принципу DDD - т.е. сначала была создана база данных со всеми таблицами и сущностями, а потом всё это нужно было реализовать в java-коде.
Честно скажу, повозиться пришлось с этим кодом немало. Зато теперь всё работает как часы и опыта в копилку прибавилось.
Итак, имеем следующую базу данных:
Не мудрствуя лукаво, создал базу такую же, как здесь.
Связь многие-ко-многим между двумя сущностями через промежуточную таблицу может быть представлена в виде двух связей многие к одному с промежуточным классом.
Создаём классы-сущности:
Параметр orphanRemoval=true сообщает фреймворку Hibernate, что тот должен навсегда удалять объекты при удалении их из коллекции.
Параметр fetch = FetchType.LAZY говорит о том, что коллекция будет загружаться только при прямом обращении к ней (по требованию) - т.н. стратегия отложенной загрузки. Для реализации такой отложенной загрузки Hibernate использует вместо объектов-коллекций сгенерированные во время выполнения заглушки, называемые прокси-объектами.
Создаём класс, который будет отображать связь многие-ко-многим между классами Tag и Post:
Что в этой промежуточной сущности интересно:
- аннотация @Immutable объявляет класс неизменяемым. Это позволяет Hibernate не проверять состояние объекта во время выталкивания контекста;
- встроенный класс PostTag.Id инкапсулирует составной ключ, состоящий из полей post_id и tag_id;
- @Embedable объявляет класс PostTag.Id встроенным в таблицу класса PostTag;
- @EmbeddedId - делает почти то же самое, что @Embedable, только для столбцов первичного ключа;
- конструктор класса PostTag, в котором производится формирование первичного ключа и двунаправленной связи многие-ко-многим между объектами Post и Tag. Эту связь очень просто сделать однонаправленной. Для этого в сущности, в которой отображение этой связи не нужно, необходимо убрать поле Set<PostTag> postTags и из конструктора класса PostTag убрать добавление созданного объекта PostTag в это поле.
Как упрощённо выглядит в java-коде создание и удаление связи между объектами Post и Tag на примере класса PostService:
Всё доволно просто:
- для создания связи нужно создать объект PostTag,
- для разрушения связи - удалить соответствующий объект PostTag из коллекции объекта Post.
Всё остальное за нас делает Hibernate.
П.с.: В классах используются аннотации фреймворков Lombok, Spring и Hibernate.
Этот интересный вопрос возник передо мной, когда на новом проекте разработка началась по принципу DDD - т.е. сначала была создана база данных со всеми таблицами и сущностями, а потом всё это нужно было реализовать в java-коде.
Честно скажу, повозиться пришлось с этим кодом немало. Зато теперь всё работает как часы и опыта в копилку прибавилось.
Итак, имеем следующую базу данных:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CREATE TABLE IF NOT EXISTS post | |
( | |
id uuid PRIMARY KEY, | |
title varchar(255) | |
); | |
CREATE TABLE IF NOT EXISTS tag | |
( | |
id uuid PRIMARY KEY, | |
name varchar(255) | |
); | |
CREATE TABLE IF NOT EXISTS post_tag | |
( | |
post_id uuid NOT NULL, | |
tag_id uuid NOT NULL, | |
PRIMARY KEY (post_id, tag_id) | |
); |
Связь многие-ко-многим между двумя сущностями через промежуточную таблицу может быть представлена в виде двух связей многие к одному с промежуточным классом.
Создаём классы-сущности:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Data | |
@Entity | |
@Table(name = "post") | |
@EqualsAndHashCode | |
public class Post { | |
@Id | |
@GeneratedValue(generator = "UUID") | |
@GenericGenerator(name = "UUID", strategy = "uuid2") | |
private UUID id; | |
@NotNull | |
@Column(name = "title") | |
private String title; | |
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval=true) | |
private Set<PostTag> postTags = new HashSet<>(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Data | |
@Entity | |
@Table(name = "tag") | |
@EqualsAndHashCode | |
public class Tag { | |
@Id | |
@GeneratedValue(generator = "UUID") | |
@GenericGenerator(name = "UUID", strategy = "uuid2") | |
private UUID id; | |
@NotNull | |
@Column(name = "name") | |
private String name; | |
@OneToMany(mappedBy = "tag", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval=true) | |
private Set<PostTag> postTags = new HashSet<>(); | |
} |
Параметр orphanRemoval=true сообщает фреймворку Hibernate, что тот должен навсегда удалять объекты при удалении их из коллекции.
Параметр fetch = FetchType.LAZY говорит о том, что коллекция будет загружаться только при прямом обращении к ней (по требованию) - т.н. стратегия отложенной загрузки. Для реализации такой отложенной загрузки Hibernate использует вместо объектов-коллекций сгенерированные во время выполнения заглушки, называемые прокси-объектами.
Создаём класс, который будет отображать связь многие-ко-многим между классами Tag и Post:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Immutable | |
@Entity | |
@Table(name = "post_tag") | |
@Getter | |
@EqualsAndHashCode | |
@NoArgsConstructor | |
public class PostTag { | |
@NoArgsConstructor | |
@AllArgsConstructor | |
@EqualsAndHashCode | |
@Embeddable | |
public static class Id implements Serializable { | |
private static final long serialVersionUID = 1L; | |
@Column(name = "post_id") | |
protected UUID postId; | |
@Column(name = "tag_id") | |
protected UUID tagId; | |
} | |
@EmbeddedId | |
protected PostTag.Id id = new PostTag.Id(); | |
@ManyToOne | |
@JoinColumn(name = "post_id", insertable = false, updatable = false) | |
private Post post; | |
@ManyToOne | |
@JoinColumn(name = "tag_id", insertable = false, updatable = false) | |
private Tag tag; | |
public PostTag(Post post, Tag tag) { | |
this.post = post; | |
this.tag = tag; | |
this.id.postId = post.getId(); | |
this.id.tagId = tag.getId(); | |
post.getPostTags().add(this); | |
tag.getPostTags().add(this); | |
} |
- аннотация @Immutable объявляет класс неизменяемым. Это позволяет Hibernate не проверять состояние объекта во время выталкивания контекста;
- встроенный класс PostTag.Id инкапсулирует составной ключ, состоящий из полей post_id и tag_id;
- @Embedable объявляет класс PostTag.Id встроенным в таблицу класса PostTag;
- @EmbeddedId - делает почти то же самое, что @Embedable, только для столбцов первичного ключа;
- конструктор класса PostTag, в котором производится формирование первичного ключа и двунаправленной связи многие-ко-многим между объектами Post и Tag. Эту связь очень просто сделать однонаправленной. Для этого в сущности, в которой отображение этой связи не нужно, необходимо убрать поле Set<PostTag> postTags и из конструктора класса PostTag убрать добавление созданного объекта PostTag в это поле.
Как упрощённо выглядит в java-коде создание и удаление связи между объектами Post и Tag на примере класса PostService:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Service | |
public class PostService { | |
@Autowared | |
private PostRepository postRepository; | |
@Autowared | |
private TagRepository tagRepository; | |
@Transactional | |
public void addTagToPost(UUID postId, UUID tagId) { | |
postRepository.findById(postId).ifPresent(post -> { | |
tagRepository.findById(tagId).ifPresent(tag -> new PostTag(post, tag)); | |
}); | |
} | |
@Transactional | |
public void deleteTagFromThePost(UUID postId, UUID tagId) { | |
postRepository.findById(postId).ifPresent(post -> { | |
post.getPostTag().removeIf(pt -> tagId.equals(pt.getTag().getId())); | |
}); | |
} | |
} |
- для создания связи нужно создать объект PostTag,
- для разрушения связи - удалить соответствующий объект PostTag из коллекции объекта Post.
Всё остальное за нас делает Hibernate.
П.с.: В классах используются аннотации фреймворков Lombok, Spring и Hibernate.