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"); Mapproperties = 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 .