Статьи

JPA Подводные камни / Ошибки

Исходя из моего опыта, как в оказании помощи командам, так и в проведении обучения, я столкнулся с некоторыми подводными камнями / ошибками, с которыми я столкнулся, что вызвало некоторые проблемы в системах на основе 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 Лоренцо Ди в блоге « Адаптация и обучение» .