Автор оригинала: Vlad Mihalcea.
Для простого отношения базы данных “многие ко многим” вы можете использовать аннотацию @ManyToMany JPA и, следовательно, скрыть таблицу соединений .
Однако иногда вам требуется больше, чем два столбца внешнего ключа в таблице объединения, и для этой цели вам необходимо заменить ассоциацию @ManyToMany двумя двунаправленными ассоциациями @OneToMany . В отличие от однонаправленного @OneToMany , двунаправленное отношение является лучшим способом сопоставления отношения базы данных “один ко многим”, для которого требуется коллекция дочерних элементов на родительской стороне
В этой статье мы рассмотрим, как можно сопоставить отношения базы данных “многие ко многим”, используя промежуточную сущность для таблицы объединения. Таким образом, мы можем сопоставить дополнительные столбцы, которые в противном случае было бы невозможно сохранить, используя аннотацию @ManyToMany JPA.
Предполагая, что у нас есть следующие таблицы базы данных:
Первое, что нам нужно, – это сопоставить составной первичный ключ, который принадлежит промежуточной таблице соединений. Как объяснено в этой статье , нам нужен @Встраиваемый тип для хранения составного идентификатора сущности:
@Embeddable
public class PostTagId
implements Serializable {
@Column(name = "post_id")
private Long postId;
@Column(name = "tag_id")
private Long tagId;
private PostTagId() {}
public PostTagId(
Long postId,
Long tagId) {
this.postId = postId;
this.tagId = tagId;
}
//Getters omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
PostTagId that = (PostTagId) o;
return Objects.equals(postId, that.postId) &&
Objects.equals(tagId, that.tagId);
}
@Override
public int hashCode() {
return Objects.hash(postId, tagId);
}
}
Есть два очень важных аспекта, которые следует учитывать при сопоставлении @встраиваемого составного идентификатора:
- Вам нужно, чтобы тип
@ВстраиваемыйбылСериализуемым - Тип
@Встраиваемыйдолжен переопределять методы equals и хэш-кода по умолчанию, основанные на двух значениях идентификатора первичного ключа.
Затем нам нужно сопоставить таблицу соединений с помощью выделенной сущности:
@Entity(name = "PostTag")
@Table(name = "post_tag")
public class PostTag {
@EmbeddedId
private PostTagId id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("postId")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("tagId")
private Tag tag;
@Column(name = "created_on")
private Date createdOn = new Date();
private PostTag() {}
public PostTag(Post post, Tag tag) {
this.post = post;
this.tag = tag;
this.id = new PostTagId(post.getId(), tag.getId());
}
//Getters and setters omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
PostTag that = (PostTag) o;
return Objects.equals(post, that.post) &&
Objects.equals(tag, that.tag);
}
@Override
public int hashCode() {
return Objects.hash(post, tag);
}
}
Налоговая сущность собирается сопоставить @OneToMany сторону для атрибута tag в Теге Post присоединиться к сущности:
@Entity(name = "Tag")
@Table(name = "tag")
@NaturalIdCache
@Cache(
usage = CacheConcurrencyStrategy.READ_WRITE
)
public class Tag {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
@OneToMany(
mappedBy = "tag",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List posts = new ArrayList<>();
public Tag() {
}
public Tag(String name) {
this.name = name;
}
//Getters and setters omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Tag tag = (Tag) o;
return Objects.equals(name, tag.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
Объект Tag помечен следующими аннотациями, относящимися к режиму гибернации:
- Аннотация
@Naturalпозволяет нам извлекать объектTagпо его бизнес-ключу. - В аннотации
@Cacheотмечена стратегия параллелизма кэша . -
@NaturalIdCacheуказывает Hibernate кэшировать идентификатор сущности, связанный с данным бизнес-ключом.
Для получения более подробной информации об аннотациях @Natural и @NaturalIdCache ознакомьтесь с этой статьей .
Имея эти аннотации на месте, мы можем извлечь объект Tag без необходимости обращаться к базе данных.
И сущность Post будет отображать @OneToMany сторону для атрибута post в теге Post присоединиться к сущности:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List tags = new ArrayList<>();
public Post() {
}
public Post(String title) {
this.title = title;
}
//Getters and setters omitted for brevity
public void addTag(Tag tag) {
PostTag postTag = new PostTag(this, tag);
tags.add(postTag);
tag.getPosts().add(postTag);
}
public void removeTag(Tag tag) {
for (Iterator iterator = tags.iterator();
iterator.hasNext(); ) {
PostTag postTag = iterator.next();
if (postTag.getPost().equals(this) &&
postTag.getTag().equals(tag)) {
iterator.remove();
postTag.getTag().getPosts().remove(postTag);
postTag.setPost(null);
postTag.setTag(null);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
Post post = (Post) o;
return Objects.equals(title, post.title);
}
@Override
public int hashCode() {
return Objects.hash(title);
}
}
Обратите внимание, что сущность Post содержит добавить тег и удалить тег служебные методы, которые необходимы каждой двунаправленной ассоциации , чтобы все стороны ассоциации оставались синхронизированными.
Хотя мы могли бы добавить те же методы добавления/удаления в сущность Tag , маловероятно, что эти ассоциации будут установлены из сущности Tag , поскольку пользователи работают с сущностями Post .
Чтобы лучше визуализировать взаимосвязи сущностей, ознакомьтесь со следующей диаграммой:
Во-первых, давайте сохраним некоторые объекты Tag , которые мы позже свяжем с Сообщением :
Tag misc = new Tag("Misc");
Tag jdbc = new Tag("JDBC");
Tag hibernate = new Tag("Hibernate");
Tag jooq = new Tag("jOOQ");
doInJPA(entityManager -> {
entityManager.persist( misc );
entityManager.persist( jdbc );
entityManager.persist( hibernate );
entityManager.persist( jooq );
});
Теперь, когда мы сохраняем две Записи сущности:
Session session = entityManager
.unwrap( Session.class );
Tag misc = session
.bySimpleNaturalId(Tag.class)
.load( "Misc" );
Tag jdbc = session
.bySimpleNaturalId(Tag.class)
.load( "JDBC" );
Tag hibernate = session
.bySimpleNaturalId(Tag.class)
.load( "Hibernate" );
Tag jooq = session
.bySimpleNaturalId(Tag.class)
.load( "jOOQ" );
Post hpjp1 = new Post(
"High-Performance Java Persistence 1st edition"
);
hpjp1.setId(1L);
hpjp1.addTag(jdbc);
hpjp1.addTag(hibernate);
hpjp1.addTag(jooq);
hpjp1.addTag(misc);
entityManager.persist(hpjp1);
Post hpjp2 = new Post(
"High-Performance Java Persistence 2nd edition"
);
hpjp2.setId(2L);
hpjp2.addTag(jdbc);
hpjp2.addTag(hibernate);
hpjp2.addTag(jooq);
entityManager.persist(hpjp2);
Hibernate создает следующие инструкции SQL:
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence 1st edition', 1)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.988', 1, 2)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.989', 1, 3)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.99', 1, 4)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.99', 1, 1)
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence 2nd edition', 2)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.992', 2, 3)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.992', 2, 4)
INSERT INTO post_tag (created_on, post_id, tag_id)
VALUES ('2017-07-26 13:14:08.992', 2, 2)
Теперь, поскольку объект Разное Тег был добавлен по ошибке, мы можем удалить его следующим образом:
Tag misc = entityManager.unwrap( Session.class )
.bySimpleNaturalId(Tag.class)
.load( "Misc" );
Post post = entityManager.createQuery(
"select p " +
"from Post p " +
"join fetch p.tags pt " +
"join fetch pt.tag " +
"where p.id = :postId", Post.class)
.setParameter( "postId", 1L )
.getSingleResult();
post.removeTag( misc );
Спящий режим, генерирующий следующие инструкции SQL:
SELECT p.id AS id1_0_0_,
p_t.created_on AS created_1_1_1_,
p_t.post_id AS post_id2_1_1_,
p_t.tag_id AS tag_id3_1_1_,
t.id AS id1_2_2_,
p.title AS title2_0_0_,
p_t.post_id AS post_id2_1_0__,
p_t.created_on AS created_1_1_0__,
p_t.tag_id AS tag_id3_1_0__,
t.name AS name2_2_2_
FROM post p
INNER JOIN
post_tag p_t ON p.id = p_t.post_id
INNER JOIN
tag t ON p_t.tag_id = t.id
WHERE p.id = 1
SELECT p_t.tag_id AS tag_id3_1_0_,
p_t.created_on AS created_1_1_0_,
p_t.post_id AS post_id2_1_0_,
p_t.created_on AS created_1_1_1_,
p_t.post_id AS post_id2_1_1_,
p_t.tag_id AS tag_id3_1_1_
FROM post_tag p_t
WHERE p_t.tag_id = 1
DELETE
FROM post_tag
WHERE post_id = 1 AND tag_id = 1
Второй запрос SELECT необходим для этой строки в методе утилиты удалить тег :
postTag.getTag().getPosts().remove(postTag);
Однако, если вам не нужно перемещаться по всем сущностям Post , связанным с Тегом , вы можете удалить коллекцию posts из сущности Tag , и эта дополнительная инструкция SELECT больше не будет выполняться.
Сущность Tag больше не будет сопоставлять Тег Post |/@OneToMany двунаправленную ассоциацию.
@Entity(name = "Tag")
@Table(name = "tag")
@NaturalIdCache
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Tag {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
public Tag() {
}
public Tag(String name) {
this.name = name;
}
//Getters omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
Tag tag = (Tag) o;
return Objects.equals(name, tag.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
Объект Post Tax и его PostTagId | @Встраиваемый идентичны предыдущему примеру.
Однако Запись организация добавить налог и удалить тег упрощены следующим образом:
public void addTag(Tag tag) {
PostTag postTag = new PostTag(this, tag);
tags.add(postTag);
}
public void removeTag(Tag tag) {
for (Iterator iterator = tags.iterator();
iterator.hasNext(); ) {
PostTag postTag = iterator.next();
if (postTag.getPost().equals(this) &&
postTag.getTag().equals(tag)) {
iterator.remove();
postTag.setPost(null);
postTag.setTag(null);
}
}
}
Остальная часть сущности Post такая же, как и в предыдущем примере, как показано на следующей диаграмме:
Вставка тега Post сущностей приведет к отображению тех же операторов SQL, что и ранее.
Но при удалении объекта Post Tax Hibernate выполнит один запрос SELECT, а также одну инструкцию DELETE:
SELECT p.id AS id1_0_0_,
p_t.created_on AS created_1_1_1_,
p_t.post_id AS post_id2_1_1_,
p_t.tag_id AS tag_id3_1_1_,
t.id AS id1_2_2_,
p.title AS title2_0_0_,
p_t.post_id AS post_id2_1_0__,
p_t.created_on AS created_1_1_0__,
p_t.tag_id AS tag_id3_1_0__,
t.name AS name2_2_2_
FROM post p
INNER JOIN
post_tag p_t ON p.id = p_t.post_id
INNER JOIN
tag t ON p_t.tag_id = t.id
WHERE p.id = 1
DELETE
FROM post_tag
WHERE post_id = 1 AND tag_id = 1
Хотя сопоставление отношений базы данных “многие ко многим” с использованием аннотации @ManyToMany, несомненно, проще, когда вам нужно сохранить дополнительные столбцы в таблице объединения, вам необходимо сопоставить таблицу объединения как выделенную сущность.
Хотя немного больше работы, ассоциация работает так же, как и ее @ManyToMany аналог, и на этот раз мы можем Перечислять коллекции, не беспокоясь о проблемах с производительностью операторов SQL.
При сопоставлении таблицы промежуточного соединения лучше сопоставлять только одну сторону как двунаправленную @OneToMany ассоциацию, так как в противном случае при удалении сущности промежуточного соединения будет выдан второй оператор SELECT.