«1. Обзор

Спецификация JPA предусматривает две разные стратегии выборки: энергичную и ленивую. Хотя ленивый подход помогает избежать ненужной загрузки данных, которые нам не нужны, иногда нам нужно прочитать данные, изначально не загруженные в закрытом контексте сохраняемости. Более того, доступ к коллекциям ленивых элементов в закрытом контексте сохранения является распространенной проблемой.

В этом руководстве мы сосредоточимся на том, как загружать данные из коллекций ленивых элементов. Мы рассмотрим три различных решения: одно с использованием языка запросов 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. Загрузка данных с помощью графа сущностей

Другое возможное решение — использовать функцию графа сущностей из 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.