Автор оригинала: Vlad Mihalcea.
Вступление
Хотя добавление отношения @OneToMany очень просто с помощью JPA и Hibernate, знание правильного способа отображения такой связи, чтобы она создавала очень эффективные операторы SQL, определенно не является тривиальной задачей.
В системе реляционных баз данных связь один ко многим связывает две таблицы на основе столбца внешнего ключа, так что запись дочерней таблицы ссылается на Первичный ключ строки родительской таблицы.
Как бы просто это ни было в реляционной базе данных, когда дело доходит до JPA, связь один ко многим база данных может быть представлена либо через @manytoon As straightforward as it might be in a relational database, when it comes to JPA, the one-to-many database association can be represented either through a @ManyToOne
Аннотация @ManyToOne позволяет сопоставить столбец внешнего ключа в сопоставлении дочерней сущности, чтобы у дочернего объекта была ссылка на объект сущности для его родительской сущности. Это наиболее естественный способ сопоставления базы данных один ко многим ассоциации баз данных, и, как правило, наиболее эффективная альтернатива тоже .
Для удобства, чтобы воспользоваться преимуществами переходов состояний сущностей и механизм грязной проверки , многие разработчики предпочитают сопоставлять дочерние объекты как коллекцию в родительском объекте, и для этой цели JPA предлагает аннотацию @OneToMany .
Как я уже много раз объяснял в своей книге , вам лучше заменить коллекции запросом, который гораздо более гибок с точки зрения производительности извлечения. Однако бывают случаи, когда сопоставление коллекции является правильным решением, и тогда у вас есть два варианта:
- однонаправленная
@OneToManyассоциация - двунаправленная
@OneToManyассоциация
Двунаправленная ассоциация требует, чтобы сопоставление дочерних сущностей предоставляло аннотацию @ManyToOne , которая отвечает за управление ассоциацией .
С другой стороны, однонаправленная @OneToMany ассоциация проще, поскольку только родительская сторона определяет отношения. В этой статье я собираюсь объяснить предостережения @OneToMany ассоциаций и то, как вы можете их преодолеть.
Существует множество способов сопоставить ассоциацию @OneToMany|/. Мы можем использовать Список или Набор. Мы также можем определить @JoinColumn аннотация тоже. Итак, давайте посмотрим, как все это работает.
Однонаправленный @OneToMany
Рассмотрим, что у нас есть следующее отображение:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
@GeneratedValue
private Long id;
private String review;
//Constructors, getters and setters removed for brevity
}
Теперь, если мы будем упорствовать один Сообщение и три Комментария(комментариев)к сообщению Now, if we persist
Post post = new Post("First post");
post.getComments().add(
new PostComment("My first review")
);
post.getComments().add(
new PostComment("My second review")
);
post.getComments().add(
new PostComment("My third review")
);
entityManager.persist(post);
Hibernate выполнит следующие инструкции SQL:
insert into post (title, id)
values ('First post', 1)
insert into post_comment (review, id)
values ('My first review', 2)
insert into post_comment (review, id)
values ('My second review', 3)
insert into post_comment (review, id)
values ('My third review', 4)
insert into post_post_comment (Post_id, comments_id)
values (1, 2)
insert into post_post_comment (Post_id, comments_id)
values (1, 3)
insert into post_post_comment (Post_id, comments_id)
values (1, 4)
Что это такое! Почему выполняется так много запросов? И в чем тут дело post_post_comment таблица в любом случае?
Ну, по умолчанию именно так работает однонаправленная @OneToMany ассоциация, и вот как это выглядит с точки зрения базы данных:
Для администратора базы данных это больше похоже на ассоциацию многие ко многим базы данных, чем на связь один ко многим , и это также не очень эффективно. Вместо двух таблиц у нас теперь три таблицы, поэтому мы используем больше места для хранения, чем необходимо. Вместо одного внешнего ключа у нас теперь их два. Однако, поскольку мы, скорее всего, будем индексировать эти внешние ключи, нам потребуется вдвое больше памяти для кэширования индекса для этой связи. Нехорошо!
Однонаправленный @OneToMany с @JoinColumn
Чтобы устранить вышеупомянутую проблему с дополнительной таблицей соединений, нам просто нужно добавить @JoinColumn в микс:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "post_id") private Listcomments = new ArrayList<>();
Аннотация @JoinColumn помогает Hibernate ( самому известному поставщику JPA ) выяснить, что в таблице post_id Внешний ключ есть столбец post_comment , который определяет эту связь.
С этой аннотацией на месте, при сохранении трех сущностей PostComment мы получаем следующий вывод SQL:
insert into post (title, id)
values ('First post', 1)
insert into post_comment (review, id)
values ('My first review', 2)
insert into post_comment (review, id)
values ('My second review', 3)
insert into post_comment (review, id)
values ('My third review', 4)
update post_comment set post_id = 1 where id = 2
update post_comment set post_id = 1 where id = 3
update post_comment set post_id = 1 where id = 4
Немного лучше , но какова цель этих трех заявлений об обновлении?
Если вы посмотрите на порядок сброса Hibernate , вы увидите, что действие сохранения выполняется до обработки элементов коллекции. Таким образом, Hibernate сначала вставляет дочерние записи без внешнего ключа, поскольку дочерняя сущность не хранит эту информацию. На этапе обработки коллекции столбец внешнего ключа обновляется соответствующим образом.
Та же логика применима к изменениям состояния коллекции, поэтому при удалении первой записи из дочерней коллекции:
post.getComments().remove(0);
Hibernate выполняет два оператора вместо одного:
update post_comment set post_id = null where post_id = 1 and id = 2 delete from post_comment where id=2
Опять же, сначала выполняется изменение состояния родительской сущности, которое запускает обновление дочерней сущности. После этого, когда коллекция будет обработана, действие удаления сироты выполнит инструкцию удаления дочерней строки.
Итак, это java.util. Установить что-нибудь другое?
Нет, это не так. Те же операторы выполняются, если вы используете аннотацию @JoinColumn для однонаправленной ассоциации @OneToMany Set.
Двунаправленный @OneToMany
Лучший способ сопоставить ассоциацию @OneToMany – это полагаться на сторону @ManyToOne для распространения всех изменений состояния сущности:
@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 comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
public void addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
}
public void removeComment(PostComment comment) {
comments.remove(comment);
comment.setPost(null);
}
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
@GeneratedValue
private Long id;
private String review;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
//Constructors, getters and setters removed for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PostComment )) return false;
return id != null && id.equals(((PostComment) o).getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
На вышеупомянутом отображении следует отметить несколько моментов:
- Ассоциация
@ManyToOneиспользуетТип выборки. ЛЕНИВЫЙпотому что в противном случае мы бы вернулись к НЕТЕРПЕЛИВОЙ выборке, что плохо сказывается на производительности . - Родительская организация,
Сообщение, содержит два полезных метода (например,Дополнение The parent entity,Post, features two utility methods (e.g.addComment and removeComment - ) which are used to synchronize both sides of the bidirectional association. You should always provide these methods whenever you are working with a bidirectional association as, otherwise, you risk
very subtle state propagation issues. иудалить комментарий), которые используются для синхронизации обеих сторон двунаправленной связи. Вы всегда должны предоставлять эти методы всякий раз, когда работаете с двунаправленной ассоциацией, так как в противном случае вы рискуетеочень тонкими проблемами распространения состояний The parent entity,Post , features two utility methods (e.g. addCommentandremoveComment ) which are used to synchronize both sides of the bidirectional association. You should always provide these methods whenever you are working with a bidirectional association as, otherwise, you risk very subtle state propagation issues.Дочерняя сущность,Оставить комментарий, реализуетравноиХэш-кодметоды. Поскольку мы не можем полагаться на
Если мы будем продолжать три Оставлять комментарии :
Post post = new Post("First post");
post.addComment(
new PostComment("My first review")
);
post.addComment(
new PostComment("My second review")
);
post.addComment(
new PostComment("My third review")
);
entityManager.persist(post);
Hibernate генерирует только один оператор SQL для каждого сохраненного Комментария к сообщению сущности:
insert into post (title, id)
values ('First post', 1)
insert into post_comment (post_id, review, id)
values (1, 'My first review', 2)
insert into post_comment (post_id, review, id)
values (1, 'My second review', 3)
insert into post_comment (post_id, review, id)
values (1, 'My third review', 4)
Если мы удалим Комментарий к сообщению :
Post post = entityManager.find( Post.class, 1L ); PostComment comment1 = post.getComments().get( 0 ); post.removeComment(comment1);
Есть только одна инструкция delete SQL, которая выполняется:
delete from post_comment where id = 2
Таким образом, двунаправленная @OneToMany ассоциация-лучший способ сопоставить отношения один ко многим базы данных, когда нам действительно нужна коллекция на родительской стороне ассоциации.
Видео на YouTube
Я также опубликовал видео на YouTube о двунаправленной ассоциации @OneToMany, так что наслаждайтесь просмотром, если вас интересует эта тема.
@ManyToOne может быть как раз достаточно
Просто потому, что у вас есть возможность использовать аннотацию @OneToMany , это не означает, что это должно быть опцией по умолчанию для каждого отношения один ко многим базы данных. Проблема с коллекциями заключается в том, что мы можем использовать их только тогда, когда количество дочерних записей довольно ограничено.
Поэтому на самом деле @OneToMany практичен только тогда, когда много означает мало. Возможно, @OneToFew было бы более подходящим названием для этой аннотации.
Как я объяснил в этом ответе на StackOverflow , вы не можете ограничить размер коллекции @OneToMany , как это было бы в случае, если бы вы использовали разбиение на страницы на уровне запросов.
Поэтому в большинстве случаев аннотация @ManyToOne на дочерней стороне-это все, что вам нужно. Но тогда, как вы получаете дочерние сущности, связанные с сущностью Post ?
Ну, все, что вам нужно, это всего лишь один JPQL-запрос:
Listcomments = entityManager.createQuery( "select pc " + "from PostComment pc " + "where pc.post.id = :postId", PostComment.class) .setParameter( "postId", 1L ) .getResultList();
Что переводится в простой SQL-запрос:
select pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
from post_comment pc
where pc.post_id = 1
Даже если коллекция больше не управляется, довольно тривиально просто добавлять/удалять дочерние сущности всякий раз, когда это необходимо. Что касается обновления дочерних объектов, механизм проверки на наличие ошибок работает просто отлично, даже если вы не используете управляемую коллекцию. Что хорошо в использовании запроса, так это то, что вы можете разбивать его на страницы так, как вам нравится, чтобы, если количество дочерних сущностей со временем будет расти, производительность приложения не пострадает.
Вывод
Как вы увидите в будущей статье, двунаправленные коллекции намного лучше, чем однонаправленные, потому что они полагаются на ассоциацию @ManyToOne , которая всегда эффективна с точки зрения сгенерированных операторов SQL.
Но тогда, даже если они очень удобны, вам не всегда нужно использовать коллекции. Ассоциация @ManyToOne является наиболее естественным и эффективным способом сопоставления отношений один ко многим базы данных.