2

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?

10
  • 1
    Probably the StackOverflowError is what you should be looking into, rather than the LazyInitializationException. It seems likely that they have the same underlying cause, but that the former approaches it more directly. Commented Nov 1 at 13:33
  • 5
    You throw the errors while printing the results. Since you have a circular reference either UserEntity is not loaded or when it loads it also reloads (again) the BerryInventoryEntity until the stack overflows. Leave FetchType at the default and write a corect JPQL to get what you want. Old timer to new guy, investigate what you don't know about before using it. Save you many hours of nonsense. Commented Nov 1 at 13:38
  • 1
    I suspect that the issue is related to code that has not yet been presented. Possibly BerryEntity, and possibly additional code in the entities that have been provided. It might even be mostly unrelated to JPA. Commented Nov 1 at 13:38
  • 2
    What is in UserEntity.toString? Lokks like you use lombok @ToString or something similar. Commented Nov 2 at 8:23
  • 1
    Revisit @Talex 's comment. The error states it can't execute UserEntity.toString() and getting the exception - so your toString implementation IS the problem. You must ensure it isn't going to turn the entire object graph into a String - Lombok is awful if you don't understand what it is generating, and likely is the cause of both your issues as it traverses the object graph recursively to build a string. Just print the entity ID if you really need a toString implementation. Commented Nov 4 at 21:05

3 Answers 3

1

Can I retrieve a BerryInventoryEntity that its UserEntity doesn't also come with a BerryInventoryEntity?

No. As long as they are linked through the relation fields (UserEntity.berryInventory & BerryInventoryEntity.user) the code will always be able to access them and lead to potential circular reference/stack overflow OR lazy initialization errors.

JPA offers you ways of stopping it from querying the DB in a circular way (LAZY, Entity Graphs), but as long as it's in the code, you can always use it.

What is the correct way of fixing this issue?

There are many ways actually, choose the one that works better for you! Pick one or more:

Fix only the immediate symptom

This can be treacherous as it may solve your problem now, only to cause more problems down the way, beware! Anyway here it goes:

Obviously the problem is caused by Lombok over-eagerly using all the entity fields for the toString() it generates. What we are using in this case is white-listing of fields to include by annotating the class with @ToString(onlyExplicitlyIncluded = true) and then each field we actually want to include in toString() with @ToString.Include. Collections and relations are almost always a no-no for inclusion.

Another way the circular dependency may come back to bite you is if you are serializing the entities themselves. I see this happening all the time, using the JPA entity as an entity (ok obviously), domain model (i.e. used in the business logic) and DTO. Single responsibility principle totally violated. Anyway, if you have the option of using separate DTOs for serialization (if ever needed), I advise you to do it, even though you will have to write a bit more code. Otherwise this is frequently solved e.g. with Jackson annotations on the entity class.

Slightly revise the design

The root of the problem is the circular dependency. Ask yourself, do you really need the "to many" side of the relationship, i.e. the berryInventory, to be explicitly declared in the UserEntity? Can't you live without this property? I have found out that, in most cases I can! Probably this is why you are using a method in the repository to fetch the berry inventory of a user - repo.findInventoryOfUser(userCreated), instead of userCreated.getBerryInventory().

UserEntity.berryInventory gone, problem gone! And you can still use the relation in your queries, perhaps with a little more effort if you relied on JPA's fancy collection operators.

Sign up to request clarification or add additional context in comments.

3 Comments

Yes, as you say, the intention of the findInventoryOfUser function was to get the inventory of a user separating it from getting it from the UserEntity itself. Getting rid of the berryInventory on UserEntity doesn't sound bad. But losing the "toMany" side of the relationship would cause that a user being deleted won't automatically delete its inventory. In the case I decide to go with that option. What do you think it would be the best way to implement said function?
Oh, indeed, this is one valid case for keeping the "to many" side of the relationship. Unless you would be willing to do some extra effort and, in the service method that handles the deletion of the user, you also manually delete the inventory. But this is a manual, error prone step (e.g. someone may forget to do it). Go for the whitelisting of fields to include in toString then. Are you facing any other problems?
Since you are using Hibernate there is a Hibernate-specific solution presented in this answer. Another answer for the same question gives a DB-based solution. I would hesitate to choose proprietary or DB solutions, but I wouldn't rule them out completely. Your choice!
1

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.

Comments

0

As you say, you have a cycle: BerryInventoryEntity -> UserEntity -> BerryInventoryEntity -> UserEntity -> ad infinitum.

The only solution is to not use EAGER and stay with the default, ie LAZY.

Then allow spring-data-jpa (without the @Query annotation) to build a query that takes the user id as a parameter rather than the UserEntity, ie:

Iterable<BerryInventoryEntity> findByUserId(Long userId);

and use it thus:

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

6 Comments

Not using EAGER seems the way to go. Still, when using the query you suggest i still get the same result. Passing a UserEntity or the Id doesn't change what the function returns. The BerryInventoryEntity will always come with an entity of User and Berry regardless.
What happens if you set the following parameter to true: spring.jpa.properties.hibernate.enable_lazy_load_no_trans ?
Tried this but didn't seem to behave differently.
JPA providers should have no problem with recursive object graphs. It is part of the requirements that parent/children relationships be bidirectional, and lazy is only a hint, not a requirement. That is the point of entities having IDs; they know what has already been fetched and can cache it when building an object graph
I'm curious about this. Now that I solved the problems I had with the toString function and other stuff that was causing trouble. When i retrieve a BerryInventoryEntity fetching the User, it comes with a UserEntity that comes with a BerryInventoryEntity and so on. I'm not getting a StackOverFlow error anymore. I'm guessing based on your comment this is normal behavior?
Yes. JPA should have no problem building an object graph - the spec requires that A->B->A that it not build a new A instance every time it is encountered in the graph. Most decent serialization mechanisms take this into account with various different schemes, but JPA has and requires IDs for the purpose.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.