Рубрики
Без рубрики

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

Узнайте, как работать с ленивой коллекцией в JPA.

Автор оригинала: baeldung.

1. Обзор

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

В этом уроке мы сосредоточимся на том, как загружать данные из коллекций ленивых элементов. Мы рассмотрим три различных решения: одно с использованием языка запросов JPA, другое с использованием графов сущностей и последнее с распространением транзакций.

2. Проблема Сбора Элементов

По умолчанию JPA использует стратегию ленивой выборки в ассоциациях типа @ElementCollection . Таким образом, любой доступ к коллекции в закрытом контексте сохранения приведет к исключению.

Чтобы понять проблему, давайте определим модель домена, основанную на отношениях между сотрудником и его телефонным списком:

@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 properties = new HashMap<>();
    properties.put("javax.persistence.fetchgraph", entityGraph);
    return em.find(Employee.class, id, properties);
}

Мы видим, что наш граф сущностей включает в себя два атрибута: имя и телефоны . Поэтому, когда JPA переводит это в SQL, он проецирует связанные столбцы.

5. Загрузка данных в Транзакционную область

Наконец, мы рассмотрим последнее решение. До сих пор мы видели, что проблема связана с жизненным циклом контекста персистентности.

Что происходит, так это то, что наш контекст сохранения является областью действия транзакции и будет оставаться открытым до тех пор, пока транзакция не завершится . Жизненный цикл транзакции охватывает период от начала до конца выполнения метода репозитория.

Итак, давайте создадим еще один тестовый случай и настроим наш контекст сохранения для привязки к транзакции, запущенной нашим методом тестирования. Мы будем держать контекст сохранения открытым до тех пор, пока тест не закончится:

@Test
@Transactional
public void whenUseTransaction_thenFetchResult() {
    Employee employee = employeeRepository.findById(1);
    assertThat(employee.getPhones().size(), is(2));
}

Аннотация @Transactional настраивает прокси-сервер транзакций вокруг экземпляра связанного тестового класса. Кроме того, транзакция связана с потоком, который ее выполняет. Учитывая параметр распространения транзакций по умолчанию, каждый контекст сохранения, созданный с помощью этого метода, присоединяется к этой же транзакции. Следовательно, контекст сохранения транзакций привязан к области транзакций метода тестирования.

6. Заключение

В этом учебном пособии мы оценили три различных решения для решения проблемы чтения данных из ленивых ассоциаций в закрытом контексте персистентности .

Во-первых, мы использовали язык запросов JPA для извлечения коллекций элементов. Затем мы определили граф сущностей для извлечения необходимых данных.

И в конечном решении мы использовали транзакцию Spring, чтобы сохранить контекст сохранения открытым и прочитать необходимые данные.

Как всегда, пример кода для этого урока доступен на GitHub .