Перейти к основному содержимому

Работа с коллекциями ленивых элементов в JPA

· 4 мин. чтения

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 .