Be Lazy With Hibernate

2024-12-15

In Spring Boot when you use JPA to connect with an SQL database, for the most part it does the right thing and manages transactions for you. However, sometimes when you fetch an entity that has a Collection on it (a Set, List, or Map) you can get an error saying something like:

 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.agonyforge.mud.demo.model.impl.MudRoom.exits: could not initialize proxy - no Session

I was testing the room editor in Agony Forge after switching from DynamoDB to PostgreSQL and ran into this. The editor was able to load the room, but when I went into the secondary screen to change the room's exits it crashed with the above exception. What is that? Why does it happen?

When Hibernate fetches an object from your database, the default way is to do it "lazily" and fetch only the main object. Any other Collections within it (basically anything that is stored in a different table) are just faked with something called a "proxy" until they are used. It's a nice performance optimization if you don't end up needing those fields because it wouldn't have to fetch them and it reduces the number of table joins required. If you actually do use one of those fields, Hibernate is supposed to go out and fetch it for you on demand so your application can pretend like it was actually there the whole time. That way it only pulls data from the database when it actually gets used.

The alternative is "eager" fetching, which tells Hibernate to grab everything instead of waiting for you to ask for it. This can be useful in situations where you are absolutely sure you are going to use the fields every time you want that object. For large objects or large numbers of objects it can slow things down a lot. You can easily tell a relation to fetch eagerly like this. There are lots of articles out there that explain how to do it:

@OneToMany(fetch=EAGER)

That by itself probably would have stopped the "failed to lazily initialize a collection" error I got, but it's not a complete explanation of what's going on and it's not really the correct answer either. All it would do in this case is hide the symptom by making it fetch exits every time we request a Room. For the Room editor it's possible that someone would edit a Room and never look at the exits at all. Furthermore, there are dozens of other places in the code that also fetch Room objects and it's likely that not all of those need the exits either. Eagerly fetching them in all of those scenarios would be inefficient and unnecessary. It's better to consider this error as a "code smell" or a warning that you have done something wrong in your code.

The "no Session" part of that error message is important. It's telling you that the database transaction has ended after the Room was fetched, but before the exits were fetched. In my case in Agony Forge, that's very puzzling due to the game's overall architecture. There is one single method called onInput() that handles every time the user types something and hits Enter. I've got that method annotated with @Transaction to make sure it creates a database transaction. There really shouldn't be any way to not be in a transaction, and more importantly the same transaction, for as long as we're processing one command from the user, from start to finish.

The first thing I did was turn on some additional logging by adding this to application.yaml:

logging:
  level:
    org:
      springframework:
        transaction: TRACE

This logging shows whenever a database transaction is started or ended, and what class is responsible for doing it. It's not great because the JPA repositories are generated code, so the log messages all appear to be from SimpleJpaRepository instead of the actual class name, but it is some additional visibility and that is helpful. I also added some log statements before and after where I fetch the room in the editor, and where I fetch the exits. That way I could see transactions as they were opened and closed in relation to when I fetched the data. Now I could see from the logs that the Room was not being fetched in the same transaction as when the exits were fetched.

Wait, what? How is that possible?

The answer to this puzzle turned out to be surprisingly simple. When I wrote the editor I was using DynamoDB instead of Hibernate, and since the code back then didn't use transactions or JPA, it worked. It fetched the Room object and stored the entire thing in the user's session map. Then the editor would just grab the fetched version of the room from the session after that without re-fetching it from the database, for as long as you stayed in the editor. Under Hibernate, this no longer works: when you open the editor it fetches the Room. But when you try to go to the exits screen, it reuses the same Room it fetched before, as the result of an earlier command, and therefore in an earlier transaction. While that may be efficient in terms of the number of database queries, that's called a "detached entity" in Hibernate. The entity was fetched in an old transaction, and Hibernate will refuse to do anything more with it by throwing LazyInitializationException.

To fix this problem I changed the code slightly so that it only stores the ID of the room in the session rather than the entire Room object. That way it has to fetch the Room each time the user sends a command. On one hand, that does mean more database queries. But now when it needs to grab the exits it can do it because the Room was fetched within that same database transaction. The larger benefit, however, is that all the other uses of Room don't have to eagerly fetch the exits if they don't need them.

Go to Comments
[ Copyright 2024 Scion Altera ]