Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

Session per request is a transactional pattern to tie the persistence session and request life-cycles together. Not surprisingly, Spring comes with its own implementation of this pattern, named OpenSessionInViewInterceptor, to facilitate working with lazy associations and therefore, improving developer productivity.

In this tutorial, first, we're going to learn how the interceptor works internally, and then, we'll see how this controversial pattern can be a double-edged sword for our applications!

2. Introducing Open Session in View

To better understand the role of Open Session in View (OSIV), let's suppose we have an incoming request:

  1. Spring opens a new Hibernate Session at the beginning of the request. These Sessions are not necessarily connected to the database.
  2. Every time the application needs a Session, it will reuse the already existing one.
  3. At the end of the request, the same interceptor closes that Session.

At first glance, it might make sense to enable this feature. After all, the framework handles the session creation and termination, so the developers don't concern themselves with these seemingly low-level details. This, in turn, boosts developer productivity.

However, sometimes, OSIV can cause subtle performance issues in production. Usually, these types of issues are very hard to diagnose.

2.1. Spring Boot

By default, OSIV is active in Spring Boot applications. Despite that, as of Spring Boot 2.0, it warns us of the fact that it's enabled at application startup if we haven't configured it explicitly:

spring.jpa.open-in-view is enabled by default. Therefore, database 
queries may be performed during view rendering.Explicitly configure 
spring.jpa.open-in-view to disable this warning

Anyway, we can disable the OSIV by using the spring.jpa.open-in-view configuration property:

spring.jpa.open-in-view=false

2.2. Pattern or Anti-Pattern?

There have always been mixed reactions towards OSIV. The main argument of the pro-OSIV camp is developer productivity, especially when dealing with lazy associations.

On the other hand, database performance issues are the primary argument of the anti-OSIV campaign. Later on, we're going to assess both arguments in detail.

3. Lazy Initialization Hero

Since OSIV binds the Session lifecycle to each request, Hibernate can resolve lazy associations even after returning from an explicit @Transactional service.

To better understand this, let's suppose we're modeling our users and their security permissions:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ElementCollection
    private Set<String> permissions;

    // getters and setters
}

Similar to other one-to-many and many-to-many relationships, the permissions property is a lazy collection.

Then, in our service layer implementation, let's explicitly demarcate our transactional boundary using @Transactional:

@Service
public class SimpleUserService implements UserService {

    private final UserRepository userRepository;

    public SimpleUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public Optional<User> findOne(String username) {
        return userRepository.findByUsername(username);
    }
}

3.1. The Expectation

Here's what we expect to happen when our code calls the findOne method:

  1. At first, the Spring proxy intercepts the call and gets the current transaction or creates one if none exists.
  2. Then, it delegates the method call to our implementation.
  3. Finally, the proxy commits the transaction and consequently closes the underlying Session. After all, we only need that Session in our service layer.

In the findOne method implementation, we didn't initialize the permissions collection. Therefore, we shouldn't be able to use the permissions after the method returns. If we do iterate on this propertywe should get a LazyInitializationException.

3.2. Welcome to the Real World

Let's write a simple REST controller to see if we can use the permissions property:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{username}")
    public ResponseEntity<?> findOne(@PathVariable String username) {
        return userService
                .findOne(username)
                .map(DetailedUserDto::fromEntity)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}

Here, we iterate over permissions during entity to DTO conversion. Since we expect that conversion to fail with a LazyInitializationException, the following test shouldn't pass:

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserControllerIntegrationTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        User user = new User();
        user.setUsername("root");
        user.setPermissions(new HashSet<>(Arrays.asList("PERM_READ", "PERM_WRITE")));

        userRepository.save(user);
    }

    @Test
    void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere() throws Exception {
        mockMvc.perform(get("/users/root"))
          .andExpect(status().isOk())
          .andExpect(jsonPath("$.username").value("root"))
          .andExpect(jsonPath("$.permissions", containsInAnyOrder("PERM_READ", "PERM_WRITE")));
    }
}

However, this test doesn't throw any exceptions, and it passes.

Because OSIV creates a Session at the beginning of the request, the transactional proxy uses the current available Session instead of creating a brand new one.

So, despite what we might expect, we actually can use the permissions property even outside of an explicit @Transactional. Moreover, these sorts of lazy associations can be fetched anywhere in the current request scope.

3.3. On Developer Productivity

If OSIV wasn't enabled, we'd have to manually initialize all necessary lazy associations in a transactional context. The most rudimentary (and usually wrong) way is to use the Hibernate.initialize() method:

@Override
@Transactional(readOnly = true)
public Optional<User> findOne(String username) {
    Optional<User> user = userRepository.findByUsername(username);
    user.ifPresent(u -> Hibernate.initialize(u.getPermissions()));

    return user;
}

By now, the effect of OSIV on developer productivity is obvious. However, it's not always about developer productivity.

4. Performance Villain

Suppose we have to extend our simple user service to call another remote service after fetching the user from the database:

@Override
public Optional<User> findOne(String username) {
    Optional<User> user = userRepository.findByUsername(username);
    if (user.isPresent()) {
        // remote call
    }

    return user;
}

Here, we're removing the @Transactional annotation since we clearly won't want to keep the connected Session while waiting for the remote service.

4.1. Avoiding Mixed IOs

Let's clarify what happens if we don't remove the @Transactional annotation. Suppose the new remote service is responding a little more slowly than usual:

  1. At first, the Spring proxy gets the current Session or creates a new one. Either way, this Session is not connected yet. That is, it's not using any connection from the pool.
  2. Once we execute the query to find a user, the Session becomes connected and borrows a Connection from the pool.
  3. If the whole method is transactional, then the method proceeds to call the slow remote service while keeping the borrowed Connection.

Imagine that during this period, we get a burst of calls to the findOne method. Then, after a while, all Connections may wait for a response from that API call. Therefore, we may soon run out of database connections.

Mixing database IOs with other types of IOs in a transactional context is a bad smell, and we should avoid it at all costs.

Anyway, since we removed the @Transactional annotation from our service, we're expecting to be safe.

4.2. Exhausting the Connection Pool

When OSIV is activethere is always a Session in the current request scope, even if we remove @Transactional. Although this Session is not connected initially, after our first database IO, it gets connected and remains so until the end of the request.

So, our innocent-looking and recently-optimized service implementation is a recipe for disaster in the presence of OSIV:

@Override
public Optional<User> findOne(String username) {
    Optional<User> user = userRepository.findByUsername(username);
    if (user.isPresent()) {
        // remote call
    }

    return user;
}

Here's what happens while the OSIV is enabled:

  1. At the beginning of the request, the corresponding filter creates a new Session.
  2. When we call the findByUsername method, that Session borrows a Connection from the pool.
  3. The Session remains connected until the end of the request.

Even though we're expecting that our service code won't exhaust the connection pool, the mere presence of OSIV can potentially make the whole application unresponsive.

To make matters even worse, the root cause of the problem (slow remote service) and the symptom (database connection pool) are unrelated. Because of this little correlation, such performance issues are difficult to diagnose in production environments.

4.3. Unnecessary Queries

Unfortunately, exhausting the connection pool is not the only OSIV-related performance issue.

Since the Session is open for the entire request lifecycle, some property navigations may trigger a few more unwanted queries outside of the transactional context. It's even possible to end up with n+1 select problem, and the worst news is that we may not notice this until production.

Adding insult to injury, the Session executes all those extra queries in auto-commit mode. In auto-commit mode, each SQL statement is treated as a transaction and is automatically committed right after it is executed. This, in turn, puts a lot of pressure on the database.

5. Choose Wisely

Whether the OSIV is a pattern or an anti-pattern is irrelevant. The most important thing here is the reality in which we're living.

If we're developing a simple CRUD service, it might make sense to use the OSIV, as we may never encounter those performance issues.

On the other hand, if we find ourselves calling a lot of remote services or there is so much going on outside of our transactional contexts, it's highly recommended to disable the OSIV altogether. 

When in doubt, start without OSIV, since we can easily enable it later. On the other hand, disabling an already enabled OSIV may be cumbersome, as we may need to handle a lot of LazyInitializationExceptions.

The bottom line is that we should be aware of the trade-offs when using or ignoring the OSIV.

6. Alternatives

If we disable OSIV, then we should somehow prevent potential LazyInitializationExceptions when dealing with lazy associations. Among a handful of approaches to coping with lazy associations, we're going to enumerate two of them here.

6.1. Entity Graphs

When defining query methods in Spring Data JPA, we can annotate a query method with @EntityGraph to eagerly fetch some part of the entity:

public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(attributePaths = "permissions")
    Optional<User> findByUsername(String username);
}

Here, we're defining an ad-hoc entity graph to load the permissions attribute eagerly, even though it's a lazy collection by default.

If we need to return multiple projections from the same query, then we should define multiple queries with different entity graph configurations:

public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "permissions")
    Optional<User> findDetailedByUsername(String username);

    Optional<User> findSummaryByUsername(String username);
}

6.2. Caveats When Using Hibernate.initialize()

One might argue that instead of using entity graphs, we can use the notorious Hibernate.initialize() to fetch lazy associations wherever we need to do so:

@Override
@Transactional(readOnly = true)
public Optional<User> findOne(String username) {
    Optional<User> user = userRepository.findByUsername(username);
    user.ifPresent(u -> Hibernate.initialize(u.getPermissions()));
        
    return user;
}

They may be clever about it and also suggest to call the getPermissions() method to trigger the fetching process:

Optional<User> user = userRepository.findByUsername(username);
user.ifPresent(u -> {
    Set<String> permissions = u.getPermissions();
    System.out.println("Permissions loaded: " + permissions.size());
});

Both approaches aren't recommended since they incur (at least) one extra query, in addition to the original one, to fetch the lazy association. That is, Hibernate generates the following queries to fetch users and their permissions:

> select u.id, u.username from users u where u.username=?
> select p.user_id, p.permissions from user_permissions p where p.user_id=?

Although most databases are pretty good at executing the second query, we should avoid that extra network round-trip.

On the other hand, if we use entity graphs or even Fetch Joins, Hibernate would fetch all the necessary data with just one query:

> select u.id, u.username, p.user_id, p.permissions from users u 
  left outer join user_permissions p on u.id=p.user_id where u.username=?

7. Conclusion

In this article, we turned our attention towards a pretty controversial feature in Spring and a few other enterprise frameworks: Open Session in View. First, we got aquatinted with this pattern both conceptually and implementation-wise. Then we analyzed it from productivity and performance perspectives.

As usual, the sample code is available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of