Как получить ассоциации FetchType.LAZY с JPA и Hibernate в Spring Controller

146

У меня есть класс Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

С отношением многие ко многим это лениво.

У меня в контроллере есть

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

А PersonRepository - это просто код, написанный в соответствии с этим руководством.

public interface PersonRepository extends JpaRepository<Person, Long> {
}

Однако в этом контроллере мне действительно нужны ленивые данные. Как я могу запустить его загрузку?

Попытка доступа к нему не удастся с

не удалось лениво инициализировать коллекцию ролей: no.dusken.momus.model.Person.roles, не удалось инициализировать прокси - нет сеанса

или другие исключения в зависимости от того, что я пытаюсь.

Мое xml-описание , на случай необходимости.

Спасибо.

Matsemann
источник
Можете ли вы написать метод, который создаст запрос для извлечения Personобъекта с заданным параметром? В том Query, что включить fetchпункт и загрузить Rolesтоже для человека.
SudoRahul

Ответы:

206

Вы должны будете сделать явный вызов для ленивого набора, чтобы инициализировать его (обычная практика - вызывать .size()для этой цели). В Hibernate есть специальный метод для this ( Hibernate.initialize()), но у JPA нет аналога. Конечно, вам нужно убедиться, что вызов выполнен, когда сеанс еще доступен, поэтому пометьте метод контроллера с помощью @Transactional. Альтернативой является создание промежуточного уровня службы между контроллером и репозиторием, который может предоставлять методы, которые инициализируют ленивые коллекции.

Обновить:

Обратите внимание, что приведенное выше решение является простым, но приводит к двум различным запросам к базе данных (один для пользователя, другой для его ролей). Если вы хотите добиться лучшей производительности, добавьте следующий метод в интерфейс репозитория Spring Data JPA:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Этот метод будет использовать предложение JPG « выборка соединения» для быстрой загрузки ассоциации ролей за один и тот же переход в базу данных и, следовательно, уменьшит снижение производительности, вызванное двумя различными запросами в вышеприведенном решении.

zagyi
источник
3
Обратите внимание, что это простое решение, но оно приводит к двум отдельным запросам к базе данных (один для пользователя, другой для его ролей). Если вы хотите добиться лучшей производительности, попробуйте написать специальный метод, который охотно выбирает пользователя и связанные с ним роли за один шаг, используя JPQL или Criteria API, как предлагали другие.
Загый
Теперь я попросил привести пример ответа Хосе, должен признать, что я не совсем понимаю.
Мацеманн
Пожалуйста, проверьте возможное решение для нужного метода запроса в моем обновленном ответе.
Zagyi
7
Интересно отметить, что если вы просто joinбез fetch, набор будет возвращен с initialized = false; поэтому по-прежнему выдает второй запрос, как только к набору обращаются. fetchявляется ключом к тому, чтобы убедиться, что отношения полностью загружены и избежать второго запроса.
FGreg
Кажется, что проблема с выполнением обоих, выборки и объединения заключается в том, что критерии предиката объединения игнорируются, и в итоге вы получаете все в списке или на карте. Если вам нужно все, затем используйте выборку, если вы хотите что-то конкретное, то объединение, но, как уже было сказано, объединение будет пустым. Это противоречит цели использования загрузки .LAZY.
К.Николас
37

Хотя это старый пост, рассмотрите возможность использования @NamedEntityGraph (Javax Persistence) и @EntityGraph (Spring Data JPA). Комбинация работает.

пример

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

а затем весеннее репо, как показано ниже

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}
rakpan
источник
1
Обратите внимание, что @NamedEntityGraphэто часть API JPA 2.1, которая не реализована в Hibernate до версии 4.3.0.
naXa
2
@EntityGraph(attributePaths = "employeeGroups")может использоваться непосредственно в репозитории данных Spring для аннотирования метода без использования @NamedEntityGraphсимвола в вашем коде, не содержащем @Entity, который легко понять при открытии репо.
Десислав Каменов
13

У вас есть несколько вариантов

  • Напишите метод в репозитории, который возвращает инициализированную сущность, как предложил RJ.

Больше работы, лучшая производительность.

  • Используйте OpenEntityManagerInViewFilter, чтобы оставить сеанс открытым для всего запроса.

Меньше работы, обычно приемлемой в веб-среде.

  • Используйте вспомогательный класс для инициализации сущностей при необходимости.

Меньше работы, полезно, когда OEMIV недоступен, например, в приложении Swing, но может быть также полезно при реализации репозитория для инициализации любого объекта за один раз.

Для последнего варианта я написал служебный класс JpaUtils для инициализации сущностей на некотором deph.

Например:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Хосе Луис Мартин
источник
Поскольку все мои запросы являются простыми вызовами REST без рендеринга и т. Д., Транзакция - это, по сути, весь мой запрос. Спасибо за ваш вклад.
Мацеманн
Как мне сделать первый? Я знаю, как написать запрос, но не умею делать то, что вы говорите. Не могли бы вы показать пример? Было бы очень полезно.
Мацеманн
zagyi привел пример в своем ответе, однако, в любом случае, спасибо, что указал мне правильное направление.
Мацеманн
Я не знаю, как будет называться ваш класс! не завершенное решение тратит впустую время других
Shady Sherif
Используйте OpenEntityManagerInViewFilter, чтобы оставить сеанс открытым для всего запроса. Плохая идея. Я бы сделал дополнительный запрос, чтобы получить все коллекции для моих сущностей.
Ян Хонски,
6

Я думаю, что вам нужен OpenSessionInViewFilter, чтобы держать сеанс открытым во время рендеринга представления (но это не слишком хорошая практика).

Михаил Николаев
источник
1
Поскольку я не использую JSP или что-то еще, просто создаю REST-API, @Transactional подойдет мне. Но будет полезно в другое время. Спасибо.
Мацеманн
@Matsemann Я знаю, что уже поздно ... но вы можете использовать OpenSessionInViewFilter даже в контроллере, так как сеанс будет существовать до тех пор, пока не будет скомпилирован ответ ...
Вишвас Шашидхар
@Matsemann Спасибо! Транзакционные аннотации сделали свое дело для меня! К вашему сведению: это даже работает, если вы просто аннотируете суперкласс класса отдыха.
отчаянный
3

Spring Data JpaRepository

Spring Data JpaRepositoryопределяет следующие два метода:

  • getOne, который возвращает прокси объекта, который подходит для установки @ManyToOneили @OneToOneродительской ассоциации при сохранении дочернего объекта .
  • findById, который возвращает объект POJO после выполнения оператора SELECT, который загружает объект из связанной таблицы

Тем не менее, в вашем случае, вы не звонили ни getOneили findById:

Person person = personRepository.findOne(1L);

Итак, я предполагаю, что findOneметод - это метод, который вы определили в PersonRepository. Однако findOneметод не очень полезен в вашем случае. Поскольку вам нужно получить коллекцию Personвместе с is roles, лучше findOneWithRolesвместо этого использовать метод.

Пользовательские методы Spring Data

Вы можете определить PersonRepositoryCustomинтерфейс следующим образом:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

И определите его реализацию следующим образом:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

Это оно!

Влад Михалча
источник
Есть ли причина, по которой вы сами написали запрос и не использовали такое решение, как EntityGraph в ответе @rakpan? Не приведет ли это к тому же результату?
Йерун Вандевельде
Затраты на использование EntityGraph выше, чем на запрос JPQL. В долгосрочной перспективе вам лучше написать запрос.
Влад Михалча
Можете ли вы уточнить накладные расходы (откуда это, заметно ли это, ...)? Потому что я не понимаю, почему есть дополнительные издержки, если они оба генерируют один и тот же запрос.
Йерун Вандевельде
1
Потому что планы EntityGraphs не кэшируются как JPQL. Это может быть значительным ударом по производительности.
Влад Михалча
1
Именно. Я должен написать статью об этом, когда у меня будет время.
Влад Михалча
1

Вы можете сделать то же самое, как это:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Просто используйте faqQuestions.getFaqAnswers (). Size () в вашем контроллере, и вы получите размер, если лениво инициализировать список, без извлечения самого списка.

neel4soft
источник