Home > Software design >  Spring Data JpaRepository throws LazyInitializationException when using method getById() but not whe
Spring Data JpaRepository throws LazyInitializationException when using method getById() but not whe

Time:11-15

in my Spring Boot (version 2.5.4) I have a service that executes one of its methods by a ExecutorService (in new Thread). in this new Thread I access some of my repositories (JpaRepositorys). I see different behavior in JpaRepository's getById() and findById(), I searched but did not found any.

this is my entity:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class LimoSet {

    @Id
    @Column(name = "_id")
    private String id;

    @Column(name = "_status")
    private String status;

    @OneToMany(mappedBy = "set", fetch = FetchType.EAGER)
    private Set<Limo> limos = new LinkedHashSet<>();

    @Column(name = "_statistics")
    private String statistics;

}

this is repository:

@Repository
public interface LimoSetRepository extends JpaRepository<LimoSet, String> {
}

and this is the service:

@Service
@Transactional
public class GeneratorService {

    private final static Logger logger = LoggerFactory.getLogger(GeneratorService.class);
    private final LimoSetRepository setRepository;
    private final ExecutorService executor = Executors.newFixedThreadPool(3);;


    public void generate(Options opts) {
        .
        .
        .

        Callable<String> task = () -> {
            try {
                this.runGenerate(opts);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            } catch (RuntimeException e) {
                e.printStackTrace();
                var set = setRepository.getById(opts.getName());
                set.setStatus(e.getMessage());
                setRepository.save(set);
            }
            return "ok";
        };
        executor.submit(task);
    }

    void runGenerate(Options opts) throws JsonProcessingException {
        .
        .
        .
        var set = setRepository.findById(opts.getName()).get(); //this is ok
        var set = setRepository.getById(opts.getName()); //this throws LazyInitializationException
        set.setStatus("GENERATED"); //the Exception is reported in this line
        setRepository.save(set);
    }
}

why findById() works but getById() does not?

CodePudding user response:

There are a few factors which sum up to the LazyInitializationException.

For primers:

get acquainted with the different semantics of findById and getById

  • findById fetches entire object
  • getById fetches object proxy

See: How do find and getReference EntityManager methods work when using JPA and Hibernate

When loading a Proxy, you need to be aware that a LazyInitializationException can be thrown if you try to access the Proxy reference after the EntityManager is closed.

Secondly, you need to understand why runGenerate runs with no active transaction, even if the class is marked as @Transactional. (Note: no active transaction is the root cause of closed EntityManager)

To make sure that the transaction is not active, see Detecting If a Spring Transaction Is Active:

assertTrue(TransactionSynchronizationManager.isActualTransactionActive());

Why parent transaction from the test is not propagated

  • runGenerate is run via executor on a separate thread
  • active transaction is kept as a thread-local, so transactions do not propagate across threads.

Why @Transactional on runGenerate has no effect?

Spring (by default) uses CGLIB proxies to enable aspects. CGLIB proxies has 2 requirements:

  • the proxied method must be public
  • you must call the method via the proxy (not via the actual object)

None of these conditions is met.

Why the transaction is active in the test itself

Spring provides a special TestExecutionListener called TransactionalTestExecutionListener, which checks if the test method is marked as @Transactional, and starts transaction if needed

How to fix

  • extract runGenerate to a service, mark it as public and @Transactional
  • autowire the new service into your test
  • more generally, take note what parts of your task operate under transactional context (catch block of your task is not transactional)
  • also more generally, learn about dirty checking. once you have your methods to run under transactional context, the save calls will be redundant.
  • Related