8

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);
    }
Ivan
  • 91
  • 1
  • 6

2 Answers2

10

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:

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:
  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:


EDIT:

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

MartinBG
  • 1,500
  • 13
  • 22
  • 1
    I think the explanation is spot on. The advice against `@Transactional` is misleading though. The same happens with manual managed transactions and transactions should be determined by your transactional needs not by some quirks of JPA. – Jens Schauder Oct 23 '19 at 05:36
1

Some extension to the - already accepted - answer:

If you use Spring Boot then it automatically enable the Open Session In View filter, which basically works as a transaction for each request.

If you want to turn off this feature add the following line to the application.properties:

spring.jpa.open-in-view=false

OSIV is really a bad idea from a performance and scalability perspective.

Selindek
  • 3,269
  • 1
  • 18
  • 25