Исходя из моего опыта, как в оказании помощи командам, так и в проведении обучения, я столкнулся с некоторыми подводными камнями / ошибками, с которыми я столкнулся, что вызвало некоторые проблемы в системах на основе Java, использующих JPA .
- Требуется открытый конструктор без аргументов
- Всегда используя двунаправленные ассоциации / отношения
- Использование
@OneToMany
для коллекций, которые могут стать огромными
Требуется публичный конструктор No-Arg
Да, JPA @Entity
требует конструктора с нулевым аргументом (или по умолчанию без аргументов). Но это можно сделать protected
. Вы не должны делать это public
. Это позволяет лучше объектно-ориентированного моделирования, так как вы не обязаны иметь общедоступный конструктор без аргументов.
Класс сущности должен иметь конструктор без аргументов. Класс сущности может иметь и другие конструкторы. Конструктор без аргументов должен быть открытым или защищенным . [Акцент мой]
— из Раздела 2.1 Спецификации Java Persistence API 2.1 (Oracle)
Если моделируемый объект имеет некоторые поля, которые необходимо инициализировать при его создании, это следует сделать через его конструктор.
ПРИМЕЧАНИЕ. Некоторые провайдеры JPA могут преодолеть отсутствующий конструктор без аргументов, добавив его во время сборки.
Допустим, мы моделируем систему бронирования номеров в отелях. В нем, вероятно, есть такие объекты, как номер, резервирование и т. Д. Для объекта резервирования, вероятно, потребуются даты начала и окончания, так как не имеет смысла создавать их без периода пребывания. Включение дат начала и окончания в качестве аргументов в конструктор резервирования позволит создать лучшую модель. Сохранение защищенного конструктора с нулевыми аргументами сделает JPA счастливым.
01
02
03
04
05
06
07
08
09
10
11
|
@Entity public class Reservation { ... public Reservation( RoomType roomType, DateRange startAndEndDates) { if (roomType == null || startAndEndDates == null ) { throw new IllegalArgumentException(...); } ... } ... protected Reservation() { /* as required by ORM/JPA */ } } |
ПРИМЕЧАНИЕ. Hibernate (поставщик JPA) позволяет сделать конструктор с нулевыми аргументами закрытым. Это делает ваш код JPA непереносимым для других поставщиков JPA.
Это также помогает добавить комментарий в конструктор с нулевыми аргументами, чтобы указать, что он был добавлен для целей JPA (техническая инфраструктура) и что он не требуется доменом (бизнес-правила / логика).
Хотя я не смог найти упомянутое в спецификации JPA 2.1, для встраиваемых классов также требуется конструктор по умолчанию (без аргументов). И так же, как сущности, требуемый конструктор без аргументов может быть protected
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Embeddable public class DateRange { ... public DateRange(Date start, Date end) { if (start == null || end == null ) { throw new IllegalArgumentException(...); } if (start.after(end)) { throw new IllegalArgumentException(...); } ... } ... protected DateRange() { /* as required by ORM/JPA */ } } |
Пример проекта DDD также скрывает конструктор no-arg, делая его областью действия пакета (см. Класс Cargo, где конструктор no-arg находится внизу).
Всегда используя двунаправленные ассоциации / отношения
Учебные материалы по JPA часто показывают двунаправленную связь. Но это не обязательно. Например, допустим, у нас есть объект заказа с одним или несколькими предметами.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Entity public class Order { @Id private Long id; @OneToMany private List<OrderItem> items; ... } @Entity public class OrderItem { @Id private Long id; @ManyToOne private Order order; ... } |
Приятно знать, что двунаправленные ассоциации поддерживаются в JPA. Но на практике это становится кошмаром обслуживания. Если позиции заказа не должны знать его родительский объект заказа, достаточно однонаправленной ассоциации (как показано ниже). ORM просто нужно знать, как назвать столбец внешнего ключа в таблице многогранников. Это обеспечивается добавлением аннотации @JoinColumn
на одной стороне ассоциации.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Entity public class Order { @Id Long id; @OneToMany @JoinColumn (name= "order_id" , ...) private List<OrderItem> items; ... } @Entity public class OrderItem { @Id private Long id; // @ManyToOne private Order order; ... } |
Делать его однонаправленным делает это проще, поскольку OrderItem
больше не нужно хранить ссылку на сущность Order
.
Обратите внимание, что могут быть моменты, когда необходима двунаправленная ассоциация. На практике это довольно редко.
Вот еще один пример. Допустим, у вас есть несколько организаций, которые ссылаются на организацию страны (например, место рождения, почтовый адрес и т. Д.). Очевидно, что эти субъекты будут ссылаться на субъект страны. Но должна ли страна ссылаться на все эти разные объекты? Скорее всего, нет.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
@Entity public class Person { @Id Long id; @ManyToOne private Country countryOfBirth; ... } @Entity public class PostalAddress { @Id private Long id; @ManyToOne private Country country; ... } @Entity public class Country { @Id ...; // @OneToMany private List<Person> persons; // @OneToMany private List<PostalAddress> addresses; } |
Таким образом, то, что JPA поддерживает двунаправленную ассоциацию, не означает, что вы должны это делать!
Использование @OneToMany
для коллекций, которые могут стать огромными
Допустим, вы моделируете банковские счета и их транзакции. Со временем аккаунт может иметь тысячи (если не миллионы) транзакций.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Entity public class Account { @Id Long id; @OneToMany @JoinColumn (name= "account_id" , ...) private List<AccountTransaction> transactions; ... } @Entity public class AccountTransaction { @Id Long id; ... } |
С учетными записями, которые имеют только несколько транзакций, не возникает никаких проблем. Но со временем, когда учетная запись содержит тысячи (если не миллионы) транзакций, вы, скорее всего, будете испытывать ошибки нехватки памяти. Итак, как лучше это отобразить?
Если вы не можете гарантировать, что максимальное количество элементов на множестве сторон ассоциации может быть загружено в память, лучше используйте @ManyToOne
на противоположной стороне ассоциации.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Entity public class Account { @Id Long id; // @OneToMany private List<AccountTransaction> transactions; ... } @Entity public class AccountTransaction { @Id Long id; @ManyToOne private Account account; ... public AccountTransaction(Account account, ...) {...} protected AccountTransaction() { /* as required by ORM/JPA */ } } |
Чтобы получить, возможно, тысячи (если не миллионы) транзакций учетной записи, используйте репозиторий, который поддерживает нумерацию страниц.
1
2
3
4
5
6
|
@Transactional public interface AccountTransactionRepository { Page<AccountTransaction> findByAccount( Long accountId, int offset, int pageSize); ... } |
Для поддержки нумерации страниц используйте setFirstResult(int)
и setMaxResults(int)
объекта Query
.
Резюме
Я надеюсь, что эти заметки помогут разработчикам избежать этих ошибок. Подвести итоги:
Требование общественности.Требуемый JPA конструктор без аргументов может бытьpublic
илиprotected
. Рассмотрите возможность сделать егоprotected
если это необходимо.Всегда используюРассмотрим однонаправленные над двунаправленными ассоциациями / отношениями.С помощьюИзбегайте@OneToMany
для коллекций, которые могут стать огромными.@ManyToOne
этого рассмотрите возможность сопоставления@ManyToOne
ManyToOne-стороны ассоциации / отношения и поддерживайте разбиение на страницы.
Ссылка: | JPA Подводные камни / Ошибки от нашего партнера JCG Лоренцо Ди в блоге « Адаптация и обучение» . |