При использовании методов getOne и findOne Spring Data JPA

155

У меня есть случай использования, когда он вызывает следующее:

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl getUserControlById(Integer id){
    return this.userControlRepository.getOne(id);
}

Обратите внимание на @Transactionalhas Propagation.REQUIRES_NEW, а хранилище использует getOne . Когда я запускаю приложение, я получаю следующее сообщение об ошибке:

Exception in thread "main" org.hibernate.LazyInitializationException: 
could not initialize proxy - no Session
...

Но если я изменю getOne(id)на findOne(id)все работает отлично.

Кстати, перед тем, как сценарий использования вызывает метод getUserControlById , он уже вызвал метод insertUserControl

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl insertUserControl(UserControl userControl) {
    return this.userControlRepository.save(userControl);
}

Оба метода являются Propagation.REQUIRES_NEW, потому что я делаю простой контроль аудита .

Я использую getOneметод, потому что он определен в интерфейсе JpaRepository, и мой интерфейс Repository расширяется оттуда, я, конечно, работаю с JPA.

Интерфейс JpaRepository происходит от CrudRepository . findOne(id)Метод определен в CrudRepository.

Мои вопросы:

  1. Почему неудачный getOne(id)метод?
  2. Когда я должен использовать getOne(id)метод?

Я работаю с другими репозиториями, и все используют getOne(id)метод, и все работают нормально, только когда я использую Propagation.REQUIRES_NEW, это не удается.

Согласно API getOne :

Возвращает ссылку на сущность с заданным идентификатором.

Согласно API findOne :

Извлекает объект по его идентификатору.

3) Когда я должен использовать findOne(id)метод?

4) Какой метод рекомендуется использовать?

Заранее спасибо.

Мануэль Джордан
источник
Особенно не следует использовать getOne () для проверки существования объекта в базе данных, потому что с getOne вы всегда получаете объект! = Null, тогда как findOne доставляет нуль.
Уве Алнер

Ответы:

137

TL; DR

T findOne(ID id)(имя в старом API) / Optional<T> findById(ID id)(имя в новом API) зависит от того, EntityManager.find()что выполняет загрузку объекта .

T getOne(ID id)полагается на EntityManager.getReference()то, что объект выполняет ленивую загрузку . Таким образом, чтобы обеспечить эффективную загрузку объекта, необходимо вызвать метод для него.

findOne()/findById()действительно более понятный и простой в использовании, чем getOne().
Так что в большинстве случаев благосклонность findOne()/findById()закончена getOne().


Изменение API

По крайней мере, 2.0версия, Spring-Data-Jpaмодифицированная findOne().
Ранее это было определено в CrudRepositoryинтерфейсе как:

T findOne(ID primaryKey);

Теперь единственный findOne()метод, который вы найдете в CrudRepositoryтом, который определен в QueryByExampleExecutorинтерфейсе как:

<S extends T> Optional<S> findOne(Example<S> example);

Наконец SimpleJpaRepository, это реализовано реализацией CrudRepositoryинтерфейса по умолчанию .
Этот метод является запросом по примеру поиска, и вы не хотите использовать его в качестве замены.

Фактически, метод с таким же поведением все еще присутствует в новом API, но имя метода изменилось.
Он был переименован из findOne()к findById()в CrudRepositoryинтерфейсе:

Optional<T> findById(ID id); 

Теперь он возвращает Optional. Что не так плохо, чтобы предотвратить NullPointerException.

Итак, фактический выбор теперь между Optional<T> findById(ID id)и T getOne(ID id).


Два разных метода, основанные на двух разных методах поиска JPA EntityManager

1) Optional<T> findById(ID id)Javadoc утверждает, что это:

Извлекает объект по его идентификатору.

Когда мы посмотрим на реализацию, мы увидим, что она зависит от EntityManager.find()выполнения поиска:

public Optional<T> findById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();

    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

И здесь em.find()приведен EntityManagerметод , объявленный как:

public <T> T find(Class<T> entityClass, Object primaryKey,
                  Map<String, Object> properties);

Его Javadoc утверждает:

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

Таким образом, получение загруженного объекта кажется ожидаемым.

2) В то время как T getOne(ID id)Javadoc утверждает (акцент мой):

Возвращает ссылку на сущность с заданным идентификатором.

Фактически, эталонная терминология действительно является платой, и JPA API не определяет какой-либо getOne()метод.
Поэтому лучше всего понять, что делает оболочка Spring, - посмотреть на реализацию:

@Override
public T getOne(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

Здесь em.getReference()приведен EntityManagerметод , объявленный как:

public <T> T getReference(Class<T> entityClass,
                              Object primaryKey);

И, к счастью, EntityManagerJavadoc лучше определил свое намерение (акцент мой):

Получить экземпляр, состояние которого можно лениво извлекать . Если запрошенный экземпляр не существует в базе данных, EntityNotFoundException генерируется при первом обращении к состоянию экземпляра . (Среде выполнения поставщика сохраняемости разрешается выбрасывать исключение EntityNotFoundException при вызове getReference.) Приложение не должно ожидать, что состояние экземпляра будет доступно после отсоединения , если только оно не было доступно приложению, пока был открыт менеджер сущностей.

Таким образом, вызов getOne()может вернуть лениво извлеченную сущность.
Здесь ленивое извлечение относится не к отношениям сущности, а к самой сущности.

Это означает, что если мы вызываем, getOne()а затем контекст постоянства закрывается, сущность может никогда не загрузиться, и поэтому результат действительно непредсказуем.
Например, если прокси-объект сериализуется, вы можете получить nullссылку как сериализованный результат или, если метод прокси-объекта вызывается, исключение, такое какLazyInitializationException выдается.
Таким образом, в такой ситуации выбрасывание EntityNotFoundExceptionэтого является основной причиной использования getOne()для обработки экземпляра, который не существует в базе данных, поскольку ситуация ошибки может никогда не выполняться, пока объект не существует.

В любом случае, чтобы гарантировать его загрузку, вы должны манипулировать сущностью во время открытия сеанса. Вы можете сделать это, вызвав любой метод на объекте.
Или лучшее альтернативное использование findById(ID id)вместо.


Почему такой неясный API?

В заключение два вопроса для разработчиков Spring-Data-JPA:

  • почему нет более четкой документации для getOne()? Сущность ленивой загрузки действительно не является деталью.

  • зачем вводить, getOne()чтобы обернуть EM.getReference()?
    Почему бы не просто придерживаться обернутой методы: getReference()? Этот EM-метод действительно очень специфичен, хотя и getOne() обеспечивает очень простую обработку.

davidxxx
источник
3
Я был озадачен, почему getOne () не выдает исключение EntityNotFoundException, но ваше «EntityNotFoundException выбрасывается при первом обращении к состоянию экземпляра» объяснил мне концепцию. Спасибо
TheCoder
Краткое содержание этого ответа: getOne()использует ленивую загрузку и выбрасывает, EntityNotFoundExceptionесли элемент не найден. findById()загружается сразу и возвращает ноль, если не найден. Поскольку есть некоторые непредсказуемые ситуации с getOne (), рекомендуется вместо этого использовать findById ().
Янак Мина
124

Основное отличие заключается в том, что getOneлениво загружен иfindOne нет.

Рассмотрим следующий пример:

public static String NON_EXISTING_ID = -1;
...
MyEntity getEnt = myEntityRepository.getOne(NON_EXISTING_ID);
MyEntity findEnt = myEntityRepository.findOne(NON_EXISTING_ID);

if(findEnt != null) {
     findEnt.getText(); // findEnt is null - this code is not executed
}

if(getEnt != null) {
     getEnt.getText(); // Throws exception - no data found, BUT getEnt is not null!!!
}
Марек Халмо
источник
1
не загружен ленивый означает, что он будет загружен только тогда, когда объект будет использоваться? поэтому я ожидаю, что getEnt будет нулевым, а код внутри второго, если не будет выполнен, не могли бы вы объяснить. Спасибо!
Даг
Если я обернулся внутри веб-службы CompletableFuture <>, я обнаружил, что вы захотите использовать findOne () вместо getOne () из-за его ленивой реализации.
Фратт
76

1. Почему метод getOne (id) не работает?

Смотрите этот раздел в документации . Вы можете переопределить уже существующую транзакцию, что может быть причиной проблемы. Однако без дополнительной информации на этот вопрос трудно ответить.

2. Когда мне следует использовать метод getOne (id)?

Без углубления во внутреннюю среду Spring Data JPA, похоже, различие заключается в механизме, используемом для извлечения сущности.

Если вы посмотрите на JavaDoc для getOne(ID)под Престолом Также :

See Also:
EntityManager.getReference(Class, Object)

похоже, что этот метод просто делегирует реализацию менеджера сущностей JPA.

Тем не менее, документы дляfindOne(ID) этого не упоминают.

Подсказка также в названиях хранилищ. JpaRepositoryявляется специфическим для JPA и, следовательно, может делегировать вызовы менеджеру сущностей, если это необходимо. CrudRepositoryне зависит от используемой технологии персистентности. Посмотрите здесь . Он используется в качестве интерфейса маркера для нескольких постоянных технологий, таких как JPA, Neo4J и т. Д.

Так что на самом деле нет разницы в этих двух методах для ваших вариантов использования, просто это findOne(ID)более общий характер, чем более специализированный getOne(ID). Какой из них вы используете, зависит от вас и вашего проекта, но я лично буду придерживаться этого, findOne(ID)поскольку это сделает ваш код менее специфичным для реализации и откроет двери для перехода к таким вещам, как MongoDB и т. Д. В будущем без чрезмерного рефакторинга :)

Донован Мюллер
источник
Спасибо, Донован, имеет смысл ваш ответ.
Мануэль Джордан
20
Я думаю, что вводить это в заблуждение очень обманчиво there's not really a 'difference' in the two methods, потому что действительно существует большая разница в том, как извлекается сущность и что вы должны ожидать от метода. Ответ ниже @davidxxx очень хорошо это подчеркивает, и я думаю, что все, кто использует Spring Data JPA, должны знать об этом. В противном случае это может привести к головной боли.
Фридберг
16

Эти getOneметоды возвращают только ссылки из БДА (отложенная загрузка). Таким образом, в основном вы находитесь за пределами транзакции (то, что Transactionalвы были объявлены в классе обслуживания, не рассматривается), и возникает ошибка.

Богдан Мата
источник
Кажется, EntityManager.getReference (Class, Object) не возвращает «ничего», поскольку мы находимся в новой области транзакции.
Мануэль Джордан
2

Я действительно нахожу очень сложным из приведенных выше ответов. С точки зрения отладки я почти потратил 8 часов, чтобы понять глупую ошибку.

У меня есть тестирование Spring + Hibernate + Dozer + Mysql проекта. Чтобы было ясно.

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

Были ли несколько книг привязаны к одному пользователю. Но в UserServiceImpl я пытался найти его с помощью getOne (userId);

public UserDTO getById(int userId) throws Exception {

    final User user = userDao.getOne(userId);

    if (user == null) {
        throw new ServiceException("User not found", HttpStatus.NOT_FOUND);
    }
    userDto = mapEntityToDto.transformBO(user, UserDTO.class);

    return userDto;
}

Результат отдыха

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 1,
        "name": "TEST_ME",
        "bookList": null
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

Приведенный выше код не получил книги, которые читал пользователь, скажем.

BookList всегда был нулевым из-за getOne (ID). После изменения найти один (ID). Результат

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 0,
        "name": "Annama",
        "bookList": [
            {
                "id": 2,
                "book_no": "The karma of searching",
            }
        ]
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

EngineSense
источник
-1

Хотя spring.jpa.open-in-view было истинным, у меня не было никаких проблем с getOne, но после установки в false я получил LazyInitializationException. Тогда проблема была решена путем замены на findById.
Хотя есть и другое решение без замены метода getOne, это @Transactional для метода, который вызывает repository.getOne (id). Таким образом, транзакция будет существовать, и сессия не будет закрыта в вашем методе, и при использовании объекта не будет никакого исключения LazyInitializationException.

Фаршад Фалаки
источник
-2

У меня была похожая проблема с пониманием, почему JpaRespository.getOne (id) не работает и выдает ошибку.

Я пошел и изменить на JpaRespository.findById (id), который требует, чтобы вы вернули Optional.

Вероятно, это мой первый комментарий к StackOverflow.

akshaymittal143
источник
К сожалению, это не дает и не дает ответа на вопрос, ни улучшает существующие ответы.
JSTL
Понятно, проблем нет.
akshaymittal143