При використанні методів getOne і findOne Spring Data JPA


154

У мене є випадок використання, коли він називає таке:

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

Пильнуй @Transactionalмає Propagation.REQUIRES_NEW і репозиторій використовує getOne . Коли я запускаю програму, я отримую таке повідомлення про помилку:

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

Але якщо я поміняю getOne(id), findOne(id)все працює добре.

BTW, перед тим, як випадок використання викликає метод getUserControlById , він вже викликав метод insertUserControl

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

Обидва способи - це розповсюдження.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 доставляє null.
Uwe Allner

Відповіді:


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)Явадок заявляє, що це:

Отримує об'єкт за його ідентифікатором.

Переглядаючи реалізацію, ми бачимо, що вона покладається на 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);

Її джавадок говорить:

Знайдіть за первинним ключем, використовуючи вказані властивості

Отже, завантаження завантаженої сутності здається очікуваним.

2) Поки штат T getOne(ID id)Явадок (акцент мій):

Повертає посилання на сутність із заданим ідентифікатором.

Насправді, довідкова термінологія насправді є бордовою, і 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():? Цей метод ЕМ насправді дуже особливий, хоча getOne() передає настільки просту обробку.


3
Мене збентежило, чому getOne () не кидає EntityNotFoundException, але ваше "EntityNotFoundException кидається, коли вперше доступ до стану екземпляра" пояснив мені цю концепцію. Спасибі
TheCoder

Короткий зміст цієї відповіді: getOne()використовує ліниве завантаження та кидає, EntityNotFoundExceptionякщо жоден елемент не знайдений. findById()завантажується відразу і повертає нуль, якщо його не знайдено. Оскільки існують деякі непередбачувані ситуації з getOne (), рекомендується використовувати замість findById ().
Janac Meena

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 буде null, а код всередині другого, якщо його не буде виконано. Чи можете ви поясніть, будь ласка. Дякую!
Дуг

Якщо загорнутий у веб-сервіс 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 дуже добре підкреслює це, і я думаю, що всі, хто використовує JPA Spring Data, повинні знати про це. В іншому випадку це може спричинити досить сильний головний біль.
Фрідберг

16

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


Здається, EntityManager.getReference (клас, об’єкт) не повертає "нічого", оскільки ми перебуваємо в новій області транзакцій.
Мануель Йордан

2

Мені справді важко знайти важкі відповіді. З точки зору налагодження я майже витратив 8 годин, щоб пізнати дурну помилку.

У мене є тестування весна + сплячка + бульдозер + проект 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

}

Вищеописаний код не отримав книги, які читає користувач, нехай скаже.

Список книг завжди був недійсним через getOne (ID). Після зміни на findOne (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

}


-1

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


-2

У мене була схожа проблема з розумінням того, чому JpaRespository.getOne (id) не працює, і видає помилку.

Я пішов і перейшов до JpaRespository.findById (id), який вимагає повернути необов'язково.

Це, мабуть, мій перший коментар щодо StackOverflow.


На жаль, це не дає і не відповідає на питання, а також не покращує існуючі відповіді.
JSTL

Я бачу, жодних проблем.
акшаміттал143
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.