Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я покажу вам лучший способ сопоставить множество ассоциаций при использовании JPA и Hibernate.
Какими бы простыми ни были аннотации JPA, не всегда очевидно, насколько они эффективны за кулисами. В этой статье я собираюсь показать вам, как лучше всего использовать JPA @ManyToMany
аннотация при использовании режима гибернации.
Модель предметной области
Предполагая, что у нас есть следующие таблицы базы данных:
Типичная связь базы данных “многие ко многим” включает две родительские таблицы, которые связаны через третью, содержащую два внешних ключа, ссылающихся на родительские таблицы.
Реализация ассоциации JPA и гибернации ManyToMany с использованием списка
Первым выбором для многих разработчиков Java является использование java.util. Список
для коллекций, которые не требуют какого-либо конкретного заказа.
@Entity(name = "Post") @Table(name = "post") public class Post { @Id @GeneratedValue private Long id; private String title; public Post() {} public Post(String title) { this.title = title; } @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Listtags = new ArrayList<>(); //Getters and setters ommitted for brevity public void addTag(Tag tag) { tags.add(tag); tag.getPosts().add(this); } public void removeTag(Tag tag) { tags.remove(tag); tag.getPosts().remove(this); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Post)) return false; return id != null && id.equals(((Post) o).getId()); } @Override public int hashCode() { return getClass().hashCode(); } } @Entity(name = "Tag") @Table(name = "tag") public class Tag { @Id @GeneratedValue private Long id; @NaturalId private String name; @ManyToMany(mappedBy = "tags") private List posts = new ArrayList<>(); public Tag() {} public Tag(String name) { this.name = name; } //Getters and setters ommitted 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); } }
В вышеупомянутом отображении следует отметить несколько аспектов, которые стоит объяснить/
Во-первых, ассоциация теги
в Записи
сущности определяет только СОХРАНЕНИЕ
и СЛИЯНИЕ
каскадные типы. Как поясняется в этой статье , УДАЛИТЬ
переход состояния сущности не имеет никакого смысла для ассоциации @ManyToMany
JPA, поскольку это может вызвать удаление цепочки, которое в конечном итоге уничтожит обе стороны ассоциации.
Как объясняется в этой статье , методы утилиты добавления/удаления являются обязательными, если вы используете двунаправленные ассоциации, чтобы убедиться, что обе стороны связи синхронизированы.
Сущность Post
использует идентификатор сущности для равенства, поскольку в ней отсутствует какой-либо уникальный бизнес-ключ. Как объяснено в этой статье , вы можете использовать идентификатор сущности для обеспечения равенства, если убедитесь, что он остается согласованным во всех переходах состояния сущности .
Объект Tag
имеет уникальный бизнес-ключ, который помечен аннотацией, специфичной для гибернации @NaturalID
. В этом случае уникальный бизнес-ключ является лучшим кандидатом для проверки на равенство .
Атрибут mappedBy
ассоциации сообщений
в Теге
сущности отмечает, что в этой двунаправленной связи Должность
юридическое лицо владеет ассоциацией. Это необходимо, поскольку только одна сторона может владеть отношениями, и изменения передаются в базу данных только с этой конкретной стороны.
Для получения более подробной информации об аннотации @Natural
ознакомьтесь с этой статьей .
Хотя сопоставление является правильным с точки зрения JPA и гибернации, с точки зрения базы данных предыдущее сопоставление многих отношений вообще неэффективно. Чтобы понять, почему это так, вам необходимо зарегистрировать и проанализировать автоматически сгенерированные инструкции SQL .
Учитывая, что у нас есть следующие организации:
final Long postId = doInJPA(entityManager -> { Post post1 = new Post("JPA with Hibernate"); Post post2 = new Post("Native Hibernate"); Tag tag1 = new Tag("Java"); Tag tag2 = new Tag("Hibernate"); post1.addTag(tag1); post1.addTag(tag2); post2.addTag(tag1); entityManager.persist(post1); entityManager.persist(post2); return post1.id; });
При удалении Тега
сущности из Сообщения
:
doInJPA(entityManager -> { Tag tag1 = new Tag("Java"); Post post1 = entityManager.find(Post.class, postId); post1.removeTag(tag1); });
Hibernate генерирует следующие инструкции SQL:
SELECT p.id AS id1_0_0_, t.id AS id1_2_1_, p.title AS title2_0_0_, t.name AS name2_2_1_, pt.post_id AS post_id1_1_0__, pt.tag_id AS tag_id2_1_0__ FROM post p INNER JOIN post_tag pt ON p.id = pt.post_id INNER JOIN tag t ON pt.tag_id = t.id WHERE p.id = 1 DELETE FROM post_tag WHERE post_id = 1 INSERT INTO post_tag ( post_id, tag_id ) VALUES ( 1, 3 )
Итак, вместо удаления только одного post_tag
запись , Hibernate удаляет все post_tag
строки, связанные с заданным post_id
, и затем повторно вставляет оставшиеся. Это совсем не эффективно, потому что это дополнительная работа для базы данных, особенно для воссоздания индексов, связанных с базовыми внешними ключами.
По этой причине не рекомендуется использовать java.util. Список
для @ManyToMany
Ассоциаций JPA.
Реализация ассоциации JPA и гибернации ManyToMany с использованием набора
Вместо Списка
, мы можем использовать Набор
.
Сообщение
сущность теги
ассоциация будет изменена следующим образом:
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Settags = new HashSet<>();
И объект Tag
подвергнется той же модификации:
@ManyToMany(mappedBy = "tags") private Setposts = new HashSet<>();
Если вы беспокоитесь об отсутствии предопределенного порядка ввода, то вам нужно использовать SortedSet
вместо Set
при предоставлении либо @SortNatural
или @SortComparator
.
Например, если объект Tag
реализует Сопоставимый
, , вы можете использовать аннотацию
@Shortnatural
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) @SortNatural private SortedSettags = new TreeSet<>();
Теперь при повторном запуске предыдущего тестового набора Hibernate генерирует следующие инструкции SQL:
SELECT p.id AS id1_0_0_, t.id AS id1_2_1_, p.title AS title2_0_0_, t.name AS name2_2_1_, pt.post_id AS post_id1_1_0__, pt.tag_id AS tag_id2_1_0__ FROM post p INNER JOIN post_tag pt ON p.id = pt.post_id INNER JOIN tag t ON pt.tag_id = t.id WHERE p.id = 1 DELETE FROM post_tag WHERE post_id = 1 AND tag_id = 3
Намного лучше! Выполняется только одна инструкция DELETE, которая удаляет связанную запись post_tag
.
Вывод
Использование JPA и Hibernate очень удобно, так как это может повысить производительность разработчиков. Однако это не означает, что вы должны жертвовать производительностью приложения.
Выбрав правильные сопоставления и шаблон доступа к данным, вы можете провести различие между приложением, которое едва выполняет обход, и приложением, которое работает со скоростью деформации.
Поэтому при использовании аннотации @ManyToMany
всегда используйте java.util. Установите
и избегайте java.util. Список
.