Почему «findById ()» возвращает прокси после вызова getOne () для той же сущности? - PullRequest
2 голосов
/ 22 октября 2019

В моей веб-заявке в сервис-макете я использую прокси для сущности "restaurant" (FetchType.Lazy в поле "restaurant").

  User user = userRepository.get(userId);
  /*
     Getting proxy here, not restaurant object
  */
  Restaurant userRestaurantRef = user.getRestaurant();

  if (userRestaurantRef != null){
     restaurantRepository.decreaseRating(userRestaurantRef.getId());
  }

  restaurantRepository.increaseRating(restaurantId);
  /*
    "getReference" invokes "getOne()"
  */
  user.setRestaurant(restaurantRepository.getReference(restaurantId));
  userRepository.save(user);

После вызова этого метода черезконтроллер в тестах, все другие методы получения RestaurantRepository (такие как findById ()) также возвращают прокси.

Но если я вызвал метод "findById ()" до метода моей службы, все в порядке.

Например:

mockMvc.perform(put(REST_URL + RESTAURANT1_ID)
                .param("time", "10:30")
                .with(userHttpBasic(USER)))
                .andExpect(status().isNoContent());

Restaurant restaurant = restaurantRepository.get(RESTAURANT1_ID);

"restaurant" - это ПРОКСИ

Restaurant restaurantBefore = restaurantRepository.get(RESTAURANT1_ID);

mockMvc.perform(put(REST_URL + RESTAURANT1_ID)
                .param("time", "10:30")
                .with(userHttpBasic(USER)))
                .andExpect(status().isNoContent());

Restaurant restaurantAfter = restaurantRepository.get(RESTAURANT1_ID);

"restaurantAfter" - это реальный объект

"get ()" в хранилище:

    @Override
    public Restaurant get(int id) {
        return repository.findById(id).orElse(null);
    }

Ответы [ 2 ]

2 голосов
/ 23 октября 2019

Есть ли у вас @Transactional аннотация для самого метода или класса обслуживания?

Это может объяснить наблюдаемое поведение.

Когда метод выполняется в транзакции, объекты приобретаются или объединяются/ сохраненные из / в базу данных кэшируются до конца транзакции (обычно это конец метода). Это означает, что любой вызов сущности с таким же идентификатором будет возвращен непосредственно из кэша и не попадет в базу данных.

Вот несколько статей о кэшировании и прокси Hibernate:

. Обратитесь к вашему примеру:

  • сначала вызовите findById(id), а затем getOne(id) возвращает ту же сущностьобъект для обоих
  • сначала вызов getOne(id), а затем findById(id) возвращает один и тот же прокси для обоих

Это потому, что они используют один и тот же id и выполняются в одной транзакции.

Документация по getOne() гласит, что она может вернуть an instance вместо ссылки (HibernateProxy), поэтому можно ожидать, что она возвращает объект:

T getOne (ID ID)

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

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

Параметры: id - не должно быть нулевым.

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

Документация по findById() с другой стороны не имеет никаких указаний в том направлении, что она может вернуть что-либо, кромеOptional сущности или пусто Optional:

Необязательный findById (идентификатор id)

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

Параметры: id - mustне быть нулевым

Возвращает: сущность с заданным идентификатором или Необязательный # empty (), если ничего не найдено

Я потратил некоторое время на поиск лучшего объяснения, но не смог найти его, поэтомуЯ не уверен, является ли это ошибкой в ​​реализации findById() или просто (плохо) документированной функцией.

В качестве обходного пути к проблеме я мог бы предложить:

  1. Не приобретайте один и тот же объект дважды в одном и том же транзакционном методе. :)
  2. Избегайте использования @Transactional, когда не нужно. Транзакциями можно управлять и вручную. Вот несколько хороших статей на эту тему:
  3. Отключение первой загруженной сущности / прокси-сервера перед (повторной) загрузкой другим способом:
import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class SomeServiceImpl implements SomeService {

    private final SomeRepository repository;
    private final EntityManager entityManager;

    // constructor, autowiring

    @Override
    public void someMethod(long id) {
        SomeEntity getOne = repository.getOne(id); // Proxy -> added to cache

        entityManager.detach(getOne); // removes getOne from the cache

        SomeEntity findById = repository.findById(id).get(); // Entity from the DB
    }
Аналогично 3-му подходу, но вместо удаления одного объекта из кэша удалите все сразу, используя метод clear():
import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class SomeServiceImpl implements SomeService {

    private final SomeRepository repository;
    private final EntityManager entityManager;

    // constructor, autowiring

    @Override
    public void someMethod(long id) {
        SomeEntity getOne = repository.getOne(id); // Proxy -> added to cache

        entityManager.clear(); // clears the cache

        SomeEntity findById = repository.findById(id).get(); // Entity from the DB
    }

Статьи по теме:


РЕДАКТИРОВАТЬ:

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

1 голос
/ 23 октября 2019

Некоторое расширение - уже принято - ответ:

Если вы используете Spring Boot, то он автоматически включает фильтр Open Session In View, который в основном работает как транзакция для каждого запроса.

Если вы хотите отключить эту функцию, добавьте следующую строку в application.properties:

spring.jpa.open-in-view=false

OSIV действительно плохая идея с точки зрения производительности и масштабируемости.

...