Anzure
Anzure

Reputation: 994

LazyInitializationException in Spring with JPA entity

I want to switch from EAGER to LAZY fetching on my JPA entity, but then I get an error. I have tried a lot of solutions and been searching for a while now for a solution that works - can't find anything that works on Google... And I don't want the entity to join fetch either.

Error: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: package.User.wallets, could not initialize proxy - no Session

Webcontroller file:

@Controller
public class AppController {

    @GetMapping("app/")
    public ModelAndView app() {

        ModelAndView mv = new ModelAndView();
        mv.setViewName("app/index");

        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        List<Wallet> wallets = user.getWallets(); // error on this line
        mv.addObject("wallets", wallets);

        return mv;
    }
}

User files:

@Entity
public class User {

    @Id
    @GeneratedValue
    @Type(type="uuid-char")
    private UUID id;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String firstName;

    @Column(nullable = false)
    private String lastName;

    @Column(nullable = false)
    private String password;

    // FetchType.EAGER solves the error, but I can't have EAGER due to performance issues.
    @OneToMany(mappedBy="user", fetch = FetchType.LAZY)
    @Fetch(value = FetchMode.SUBSELECT)
    private List<Wallet> wallets;

    // ** getters and setters **

}

@Repository
public interface UserRepo extends JpaRepository<User, UUID> {

    Optional<User> findByEmail(String email);

}

Wallet files:

@Entity
public class Wallet {

    @Id
    @GeneratedValue
    @Type(type="uuid-char")
    private UUID id;

    @ManyToOne
    @JoinColumn(name="user_id", nullable=false)
    private User user;

    @OneToOne
    @JoinColumn(name="currency_id", nullable=false)
    private Currency currency;

    @Column(nullable = false)
    private double balance;

    // ** getters and setters **

}

@Repository
public interface WalletRepo extends JpaRepository<Wallet, UUID> {

    Optional<Wallet> findByUserAndCurrency(User user, Currency currency);

}

Thanks for reading all this, I would really appreciate any replies.

Upvotes: 0

Views: 2550

Answers (3)

Anzure
Anzure

Reputation: 994

This was caused by that the User object in SecurityContextHolder.getContext().getAuthentication().getPrincipal() had already closed the connection even though @Transaction was annotated in Controller class.

I had to change the Controller class like this:

    @GetMapping("app/")
    public ModelAndView app() {

        ModelAndView mv = new ModelAndView();
        mv.setViewName("app/index");

        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user = userRepo.findByUsername(userDetails.getUsername()).orElse(null);

        List<Wallet> wallets = user.getWallets(); // no error anymore
        mv.addObject("wallets", wallets);

        return mv;
    }

Hope this can help anyone else, because I couldn't find this mistake anywhere.

Upvotes: 1

mrg
mrg

Reputation: 332

You have to add org.springframework.transaction.annotation.Transactional above your AppController class and then lazy loading will work.

Example:

import org.springframework.transaction.annotation.Transactional;

@Controller
@Transactional
public class AppController {

    @GetMapping("app/")
    public ModelAndView app() {

        ModelAndView mv = new ModelAndView();
        mv.setViewName("app/index");

        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        List<Wallet> wallets = user.getWallets(); // error on this line
        mv.addObject("wallets", wallets);

        return mv;
    }
}

If it didn't work, you probably don't have enabled transaction management (this is normally configured by Spring Boot auto configuration). Try to add @EnableTransactionManagement e.g. above the main application class, where @SpringBootApplication annotation is used.

You could also try that line below:

Hibernate.initialize(user.getWallets());

Upvotes: 1

ZINE Mahmoud
ZINE Mahmoud

Reputation: 1332

The origin of your problem:

By default hibernate lazily loads the collections (relationships) which means whenver you use the collection in your code the hibernate gets that from database, now the problem is that you are getting the collection in your controller (where the JPA session is closed).This is the line of code that causes the exception (where you are loading the comments collection) :

List<Wallet> wallets = user.getWallets();

Solution

you have this on your User entity

 @OneToMany(mappedBy="user", fetch = FetchType.LAZY)
    @Fetch(value = FetchMode.SUBSELECT)
    private List<Wallet> wallets;

use this

@OneToMany(fetch = FetchType.EAGER, mappedBy = "user", cascade = CascadeType.ALL)
private List<Wallet> wallets;

Collections are lazy-loaded by default

UPDATED Solution

This particular case will be solved by adding toString() in User to re-define over Lombok toString()

keep your fetchType.LAZY instead fetchType.EAGER and add toString method to your User Entity

@Override
public String toString() {
        return "User [id=" + id + ", email=" + email+ ", firstName=" + firstName+ ", lastName=" + lastName + ", password=" + password+ "]";
    }

and add to you Wallet Entity :

  @ManyToOne(fetch=FetchType.EAGER)
  @JoinColumn(name="user_id", nullable=false)
  private User user;

try to add @Transactional to your controller too

Upvotes: 1

Related Questions