1. Обзор
Спецификация JPA предоставляет две разные стратегии выборки: нетерпеливую и ленивую. Хотя ленивый подход помогает избежать ненужной загрузки данных, которые нам не нужны, иногда нам нужно прочитать данные, изначально не загруженные в закрытом Persistence Context . Более того, доступ к коллекциям ленивых элементов в закрытом контексте сохранения является распространенной проблемой.
В этом руководстве мы сосредоточимся на том, как загружать данные из коллекций отложенных элементов. Мы рассмотрим три различных решения: одно с использованием языка запросов JPA, другое с использованием графов сущностей и последнее с распространением транзакций.
2. Проблема сбора элементов
По умолчанию JPA использует стратегию отложенной выборки в ассоциациях типа @ElementCollection
. Таким образом, любой доступ к коллекции в закрытом Persistence Context приведет к исключению.
Чтобы понять проблему, давайте определим модель предметной области на основе связи между сотрудником и его списком телефонов:
@Entity
public class Employee {
@Id
private int id;
private String name;
@ElementCollection
@CollectionTable(name = "employee_phone", joinColumns = @JoinColumn(name = "employee_id"))
private List phones;
// standard constructors, getters, and setters
}
@Embeddable
public class Phone {
private String type;
private String areaCode;
private String number;
// standard constructors, getters, and setters
}
В нашей модели указано, что у сотрудника может быть много телефонов. Список телефонов представляет собой набор встраиваемых типов . Давайте используем репозиторий Spring с этой моделью:
@Repository
public class EmployeeRepository {
public Employee findById(int id) {
return em.find(Employee.class, id);
}
// additional properties and auxiliary methods
}
Теперь давайте воспроизведем проблему на простом тестовом примере JUnit:
public class ElementCollectionIntegrationTest {
@Before
public void init() {
Employee employee = new Employee(1, "Fred");
employee.setPhones(
Arrays.asList(new Phone("work", "+55", "99999-9999"), new Phone("home", "+55", "98888-8888")));
employeeRepository.save(employee);
}
@After
public void clean() {
employeeRepository.remove(1);
}
@Test(expected = org.hibernate.LazyInitializationException.class)
public void whenAccessLazyCollection_thenThrowLazyInitializationException() {
Employee employee = employeeRepository.findById(1);
assertThat(employee.getPhones().size(), is(2));
}
}
Этот тест выдает исключение, когда мы пытаемся получить доступ к списку телефонов, потому что контекст сохраняемости закрыт .
Мы можем решить эту проблему, изменив стратегию выборки @ElementCollection
, чтобы использовать нетерпеливый подход . Однако жадное извлечение данных не обязательно является лучшим решением , поскольку данные телефона всегда будут загружаться, независимо от того, нужно нам это или нет.
3. Загрузка данных с помощью языка запросов JPA
Язык запросов JPA позволяет нам настраивать проецируемую информацию. Поэтому мы можем определить новый метод в нашем EmployeeRepository
для выбора сотрудника и его телефонов:
public Employee findByJPQL(int id) {
return em.createQuery("SELECT u FROM Employee AS u JOIN FETCH u.phones WHERE u.id=:id", Employee.class)
.setParameter("id", id).getSingleResult();
}
Приведенный выше запрос использует операцию внутреннего соединения для получения списка телефонов для каждого возвращенного сотрудника.
4. Загрузка данных с помощью Entity Graph
Другое возможное решение — использовать функцию графа сущностей из JPA. Граф сущностей позволяет нам выбирать, какие поля будут проецироваться запросами JPA. Давайте определим еще один метод в нашем репозитории:
public Employee findByEntityGraph(int id) {
EntityGraph entityGraph = em.createEntityGraph(Employee.class);
entityGraph.addAttributeNodes("name", "phones");
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
return em.find(Employee.class, id, properties);
}
Мы видим, что наш граф сущностей включает в себя два атрибута: имя и телефоны . Итак, когда JPA преобразует это в SQL, он проецирует связанные столбцы.
5. Загрузка данных в транзакционную область
Наконец, мы собираемся изучить еще одно решение. До сих пор мы видели, что проблема связана с жизненным циклом Persistence Context.
Что происходит, так это то, что наш Persistence Context находится в области транзакции и будет оставаться открытым до завершения транзакции . Жизненный цикл транзакции охватывает от начала до конца выполнения метода репозитория.
Итак, давайте создадим еще один тестовый пример и настроим наш Persistence Context для привязки к транзакции, запущенной нашим тестовым методом. Мы оставим Persistence Context открытым до окончания теста:
@Test
@Transactional
public void whenUseTransaction_thenFetchResult() {
Employee employee = employeeRepository.findById(1);
assertThat(employee.getPhones().size(), is(2));
}
Аннотация @Transactional
настраивает транзакционный прокси вокруг экземпляра связанного тестового класса. Более того, транзакция связана с выполняющим ее потоком. Учитывая параметр распространения транзакции по умолчанию, каждый контекст сохраняемости, созданный с помощью этого метода, присоединяется к этой же транзакции. Следовательно, контекст сохранения транзакции привязан к области транзакции тестового метода.
6. Заключение
В этом руководстве мы оценили три разных решения для решения проблемы чтения данных из ленивых ассоциаций в закрытом контексте персистентности .
Во-первых, мы использовали язык запросов JPA для выборки коллекций элементов. Затем мы определили граф объектов для получения необходимых данных.
И, в окончательном решении, мы использовали транзакцию Spring, чтобы сохранить контекст сохранения открытым и прочитать необходимые данные.
Как всегда, пример кода для этого руководства доступен на GitHub .