Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why "findById()" returns proxy after calling getOne() on same entity?

In my web-apllication, in service-layout, I'm using proxy for the "restaurant" entity (FetchType.Lazy on "restaurant" field).

  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);

After calling this method via controller in tests, all other RestaurantRepository's getting methods (such as findById()) returns proxy also.

But, if I called "findById()" method before my service's method, it's all OK.

For example:

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

Restaurant restaurant = restaurantRepository.get(RESTAURANT1_ID);

"restaurant" is PROXY

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" is real Object

"get()" into repository:

    @Override
    public Restaurant get(int id) {
        return repository.findById(id).orElse(null);
    }
like image 602
Ivan Avatar asked Oct 22 '19 17:10

Ivan


People also ask

What is difference between getOne and findById?

Both findById() and getOne() methods are used to retrieve an object from underlying datastore. But the underlying mechanism for retrieving records is different for both these methods, infact getOne() is lazy operation which does not even hit the database.

What does findById return in JPA?

Its findById method retrieves an entity by its id. The return value is Optional<T> . Optional<T> is a container object which may or may not contain a non-null value. If a value is present, isPresent returns true and get returns the value.

What is difference between getById and findById in JPA?

As a consequence, findById() returns the actual object and getById returns a reference of the entity.

What is getOne method in spring boot?

The getOne() method is used to retrieves an entity based on id and it is available in the JpaRepository interface.


1 Answers

Do you have @Transactional annotation on the method or service class itself?

This could explain the observed behavior.

When a method is executed in a transaction, entities acquired or merged/saved from/to the database are cached until the end of the transaction (usually the end of the method). That means that any call for entity with same ID will be returned directly from the cache and will not hit the database.

Here are some articles on Hibernate's caching and proxies:

  • Understanding Hibernate First Level Cache with Example
  • How does a JPA Proxy work and how to unproxy it with Hibernate
  • The best way to initialize LAZY entity and collection proxies with JPA and Hibernate

Back to your example:

  • call findById(id) first and then getOne(id) returns the same entity object for both
  • call getOne(id) first and then findById(id) returns the same proxy for both

That's because they share the same id and are executed in the same transaction.

Documentation on getOne() states that it could return an instance instead of reference (HibernateProxy), so having it returning an entity could be expected:

T getOne(ID id)

Returns a reference to the entity with the given identifier.

Depending on how the JPA persistence provider is implemented this is very likely to always return an instance and throw an EntityNotFoundException on first access. Some of them will reject invalid identifiers immediately.

Parameters: id - must not be null.

Returns: a reference to the entity with the given identifier.

Documentation on findById() from the other hand does not have any hints in the direction that it could return anything but Optional of entity or empty Optional:

Optional findById(ID id)

Retrieves an entity by its id.

Parameters: id - must not be null.

Returns: the entity with the given id or Optional#empty() if none found

I've spend some time looking for a better explanation, but failed to find one so I'm not sure if it is a bug in the implementation of findById() or just a not (well) documented feature.

As workarounds to the problem I could suggest:

  1. Do not acquire the same entity twice in the same transactional method. :)
  2. Avoid using @Transactional when not need. Transactions can be managed manually too. Here are some good articles on that subject:
    • 5 common Spring @Transactional pitfalls
    • Spring Transactional propagation modes
    • Spring pitfalls: transactional tests considered harmful.
  3. Detach first loaded entity/proxy before (re-)loading using the other method:
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
    }
  1. Similar to the 3rd approach, but instead of removing a single object from the cache, remove all at once using the clear() method:
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
    }

Related articles:

  • When use getOne and findOne methods Spring Data JPA
  • Hibernate Session: evict() and merge() Example
  • clear(), evict() and close() methods in Hibernate
  • JPA - Detaching an Entity Instance from the Persistence Context
  • Difference between getOne and findById in Spring Data JPA?

EDIT:

Here is a simple project demonstrating the problem or the feature (depending on the point of view).

like image 130
MartinBG Avatar answered Sep 19 '22 01:09

MartinBG