javaspring-bootjpaspring-data-jpa

JPA recursive hierarchy read - StackOverflowError with FetchType.EAGER but LazyInitializationException with FetchType.LAZY


I'm working with these entities:

UserEntity:

@Entity
@Table (name = "users", uniqueConstraints = @UniqueConstraint(columnNames={"name"}))
public class UserEntity {

    @Id
    @GeneratedValue (strategy = GenerationType.SEQUENCE, generator = "user_id_seq")
    private Long id;

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

    private int level;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
    private Set<BerryInventoryEntity> berryInventory;

BerryInventoryEntity:

@Entity
@Table (name = "berry_inventory")
public class BerryInventoryEntity {

    @Id
    @GeneratedValue (strategy = GenerationType.SEQUENCE, generator = "inventory_id_seq")
    private Long id;

    @ManyToOne (optional = false)
    @JoinColumn (name = "user_id")
    private UserEntity user;

    @ManyToOne (optional = false)
    @JoinColumn (name = "berry_id")
    private BerryEntity berry;

    private int quantity;
}

I'm testing the repository of BerryInventory and I ran into a problem. I created an instance of BerryInventoryEntity and saved it using the repository:

BerryInventoryEntity berryInventory = BerryInventoryEntity.builder()
        .berry(berry)
        .user(user)
        .quantity(1)
        .build();

berryInventoryRepository.save(berryInventory);

Then I used this query to retrieve the BerryInventoryEntity I just saved:

@Query("SELECT b FROM BerryInventoryEntity b WHERE b.user = :user")
Iterable<BerryInventoryEntity> findInventoryOfUser(UserEntity user);

Like this:

Iterable<BerryInventoryEntity> berryInventoryCreated = berryInventoryRepository.findInventoryOfUser(userCreated);

But then U get two different errors depending on the FetchType in the @OneToMany relationship indicated in UserEntity.

If the FetchType is "LAZY" the UserEntity comes with an instance of BerryInventoryEntity that throws the exception:

Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate com.dpr.berry.domain.entities.UserEntity.toString()

But if I change it to "EAGER" then a StackOverFlowError is thrown because UserEntity contains a list of BerryInventoryEntity that also contains UserEntity and so on.

Can I retrieve a BerryInventoryEntity that its UserEntity doesn't also come with a BerryInventoryEntity? What is the correct way of fixing this issue?


Solution

  • Thank you everyone who helped. This is how I solved it.

    First, "LazyInitializationException":

    Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate com.dpr.berry.domain.entities.UserEntity.toString()

    The problem here wasn't leaving the FetchType on the "OneToMany" part of the relationship as "LAZY", since that's the default anyway.

    What was causing this problem was the Lombok @Data annotation. It wasn't behaving correctly when handling the variables that referenced the related entities.

    The solution to this was writing my own toString for each entity, where i only included the ID + Name. Also, I got rid of the @Data annotations and instead I'm just using the @Getter and @Setter annotations.

    Second, "StackOverflowError":

    This problem happened when I set the FetchType on the "OneToMany" relationship as "EAGER".

    The solution to this was leaving it as default, which is "LAZY", on the two sides of the "OneToMany" relationship. And also, setting it as "LAZY" on the "ManyToOne" part of the relationship, since the default there is "EAGER":

    UserEntity:

    @OneToMany (mappedBy = "user")
    @OnDelete (action = OnDeleteAction.CASCADE)
    private Set<BerryInventoryEntity> berryInventory;
    

    BerryEntity:

    @OneToMany (mappedBy = "berry")
    @OnDelete (action = OnDeleteAction.CASCADE)
    private Set<BerryInventoryEntity> berryInventory;
    

    BerryInventoryEntity:

    @ManyToOne (optional = false, fetch = FetchType.LAZY)
    @JoinColumn (name = "user_id")
    private UserEntity user;
    
    @ManyToOne (optional = false, fetch = FetchType.LAZY)
    @JoinColumn (name = "berry_id")
    private BerryEntity berry;
    

    Also, i added LEFT JOIN FETCH b.berry to the findInventoryOfUser method on BerryInventoryRepository:

    @Query ("SELECT b FROM BerryInventoryEntity b LEFT JOIN FETCH b.berry WHERE b.user = :user")
    Iterable<BerryInventoryEntity> findInventoryOfUser(UserEntity user);
    

    So this way I only fetch the data i need.