Matthias
Matthias

Reputation: 2785

Spring Boot Data JPA doing lazy loading - not on a relation but on the loaded entity?

I have just come upon something that I can't describe in any other way than bizarre.

I have a service that is supposed to do this:

  1. it gets passed an external identifier of a customer
  2. it looks up the customer's internal ID
  3. then loads and returns the customer

I'm using optionals as there is a potential chance that external identifiers can't be resolved.

@Transactional(readOnly = true)
public Optional<Customer> getCustomerByExternalReference(String externalId, ReferenceContext referenceContext) {
    return externalIdMappingService.resolve(externalId, referenceContext, InternalEntityType.CUSTOMER)
        .map(x->new CustomerId(x.getTarget()))
        .map(customerRepository::getById);
}

what's noteworthy is here is that: externalIdMappingRepository.resolve returns an Optional<ExternalIdReference> object. If that is present, I attempt to map it to a customer that I then look up from the database. customerRepository is a regular spring data JPA repository (source code below)

However, when trying to access properties from Customer outside the service, I get an exception like this:

org.hibernate.LazyInitializationException: could not initialize proxy [Customer#Customer$CustomerId@3e] - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176)
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322)
    at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
    at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
    at Customer$HibernateProxy$R0X59vMR.getIdName(Unknown Source)
    at CustomerApiModel.<init>(CustomerApiModel.java:27)

I understand that this means, that Hibernate decided to lazy load that entity. Once outside the transactional boundaries of the service, it's not able to load the data for that object anymore.

My Question is: Why does Hibernate/Spring Data try a lazy fetching strategy when I essentially just load a specific object by ID from a Spring Data Repository and how I can disable this behaviour the right way.

I'm aware that there is a couple of workarounds to fix the problem (such as allowing hibernate to open sessions at will, or to access properties of that object inside the service). I'm not after such fixes. I want to understand the issue and want to ensure that lazy fetching only happens when it's supposed to happen

Here's the code for customer (just the part that I think is helpful)

@Entity
@Table(name="customer")
@Getter
public class Customer  {
    @EmbeddedId
    private CustomerId id;

    @Embeddable
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    public static class CustomerId implements Serializable {

        private long id;

        public long asLong() {
            return id;
        }

    }
}

and here's the source code of the repository:

public interface CustomerRepository extends Repository<Customer, CustomerId> {
    List<Customer> findAll();       
    Customer getById(CustomerId id);
    Optional<Customer> findOneById(CustomerId id);
    Optional<Customer> findOneByIdName(String idName);
}

Upvotes: 3

Views: 3718

Answers (1)

fladdimir
fladdimir

Reputation: 1365

By declaring the method Customer getById(CustomerId id); in your CustomerRepository interface, you chose to let your repostory selectively expose the corresponding method with the same signature from the standard spring-data repository methods, as explained by the Repository java doc:

Domain repositories extending this interface can selectively expose CRUD methods by simply declaring methods of the same signature as those declared in CrudRepository.

Different to what the doc says, this also includes methods from JpaRepository.

In the case of Customer getById(CustomerId id);, you therefore invoke the JpaRepository method with the same signature: T getOne(ID id);, which only invokes EntityManager#getReference , as suggested by it's doc:

[...] 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 {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers immediately. [...]
@see EntityManager#getReference(Class, Object) for details on when an exception is thrown.

When calling EntityManager#getReference, Hibernate first returns a non-initialized proxy of the Entity without executing any SQL statement at all, which is why your method only returns the non-initialized entity.

To fix this, you could change your service logic as follows:

@Transactional(readOnly = true)
public Optional<Customer> getCustomerByExternalReference(String externalId, ReferenceContext referenceContext) {
  return externalIdMappingService.resolve(externalId, referenceContext, InternalEntityType.CUSTOMER)
    .map(x->new CustomerId(x.getTarget()))
    .map(id -> customerRepository.findOneById(id).get()); // <-- changed call
}

This way, spring-data would invoke CrudRepository#findById, which would internally call EntityManager#find and therefore return an initialized entity (or an empty Optional if none was found in the DB).

Related:
When use getOne and findOne methods Spring Data JPA
Why "findById()" returns proxy after calling getOne() on same entity? (attention when using getOne and findById in the same transaction)

Upvotes: 3

Related Questions