A Guide to Spring’s Open Session In View – Spring的公开会议指南(A Guide to Spring’s Open Session In View

最后修改: 2019年 11月 3日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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.

每个请求的会话是一种事务性模式,将持久化会话和请求的生命周期联系在一起。毫不奇怪,Spring自带了这种模式的实现,名为OpenSessionInViewInterceptor,以方便与懒惰关联的工作,从而提高开发人员的工作效率。

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

2.在视图中介绍公开会议

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

为了更好地理解Open Session in View(OSIV)的作用,让我们假设有一个传入的请求。

  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.

然而,有时,OSIV可能会在生产中造成微妙的性能问题。通常情况下,这些类型的问题是很难诊断的。

2.1. Spring Boot

2.1.Spring启动

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:

默认情况下,OSIV在Spring Boot应用程序中是激活的。尽管如此,从Spring Boot 2.0开始,如果我们没有明确配置它,它就会在应用程序启动时警告我们它已经启用。

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配置属性禁用OSIV。

spring.jpa.open-in-view=false

2.2. Pattern or Anti-Pattern?

2.2.模式还是反模式?

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.

对于OSIV,人们的反应一直不一。支持OSIV的阵营的主要论点是开发者的生产力,特别是在处理lazy association时。

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.

另一方面,数据库性能问题是反OSIV运动的主要论据。稍后,我们将对这两个论点进行详细评估。

3. Lazy Initialization Hero

3.懒惰的初始化英雄

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

由于OSIV将Session 生命周期与每个请求绑定,Hibernate甚至在从显式的@Transactional服务返回后也能解决懒惰关联。

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.

与其他一对多和多对多的关系类似,permissions属性是一个懒散的集合。

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

然后,在我们的服务层实现中,让我们使用@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

3.1.期望

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

下面是我们的代码调用findOne方法时期望发生的情况。

  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.

findOne方法的实现中,我们没有初始化permissionscollection。因此,在方法返回后,我们应该无法使用permissions。如果我们真的对这个属性进行迭代我们应该得到一个LazyInitializationException。

3.2. Welcome to the Real World

3.2.欢迎来到现实世界

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

让我们写一个简单的REST控制器,看看我们是否可以使用permissionsproperty。

@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:

在这里,我们在实体到DTO的转换过程中遍历permissions 。由于我们预计转换会以LazyInitializationException的方式失败,所以下面的测试不应该通过。

@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.

由于OSIV在请求开始时创建了一个Session,事务代理使用当前可用的Session,而不是创建一个全新的

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.

因此,尽管我们可能期望,我们实际上可以使用permissions property,甚至在显式@Transactional之外。此外,这类懒惰的关联可以在当前请求范围内的任何地方被获取。

3.3. On Developer Productivity

3.3.关于开发者的生产力

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:

如果没有启用OSIV,我们就必须在事务性上下文中手动初始化所有必要的懒惰关联。最基本的(通常也是错误的)方法是使用Hibernate.initialize() 方法。

@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.

到现在为止,OSIV对开发者生产力的影响是显而易见的。然而,这并不总是关于开发者的生产力。

4. Performance Villain

4.表演小人

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.

在这里,我们删除了@Transactional 注解,因为我们显然不希望在等待远程服务的同时保持连接的Session

4.1. Avoiding Mixed IOs

4.1.避免混合IO

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:

让我们澄清一下,如果我们不删除@Transactional 注解会发生什么。假设新的远程服务的响应速度比平时要慢一些:

  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.

想象一下,在这段时间内,我们得到了对findOne方法的大量调用。然后,在一段时间后,所有连接都可能等待来自该API调用的响应。因此,我们可能很快就会耗尽数据库连接。

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

在事务性背景下将数据库IO与其他类型的IO混在一起是一种不好的味道,我们应该不惜一切代价避免它。

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

无论如何,既然我们从我们的服务中删除了@Transactional 注释,我们期望是安全的

4.2. Exhausting the Connection Pool

4.2.用尽连接池

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.

当OSIV处于活动状态时在当前请求范围内总是有一个Session,即使我们移除@Transactional。尽管这个Session最初没有被连接,但在我们的第一个数据库IO之后,它被连接了,并且一直保持到请求结束。

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

因此,在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:

下面是OSIV启用时的情况。

  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.

尽管我们期望我们的服务代码不会耗尽连接池,但仅仅是OSIV的存在就有可能使整个应用程序失去响应。

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

4.3.不必要的查询

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

不幸的是,用尽连接池并不是唯一与OSIV有关的性能问题。

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.

由于Session在整个请求生命周期中是开放的,一些属性导航可能会在事务性上下文之外再触发一些不必要的查询。甚至有可能最终出现n+1选择问题,而最糟糕的消息是,我们可能在生产之前不会注意到这一点。

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.

雪上加霜的是,Session自动提交模式中执行了所有这些额外的查询。在自动提交模式下,每个SQL语句都被视为一个事务,并在执行后立即自动提交。这反过来又给数据库带来了很大的压力。

5. Choose Wisely

5.明智的选择

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.

OSIV是一种模式还是一种反模式并不重要。这里最重要的是我们所处的现实。

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

如果我们正在开发一个简单的CRUD服务,使用OSIV可能是有意义的,因为我们可能永远不会遇到这些性能问题。

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. 

另一方面,如果我们发现自己调用了很多远程服务,或者有很多事情在我们的事务性上下文之外发生,强烈建议完全禁用OSIV。

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.

如果有疑问,开始时不要使用OSIV,因为我们以后可以很容易地启用它。另一方面,禁用已经启用的OSIV可能很麻烦,因为我们可能需要处理大量的LazyInitializationExceptions.

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

底线是,在使用或忽略OSIV时,我们应该意识到权衡利弊。

6. Alternatives

6.替代品

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.

如果我们禁用OSIV,那么在处理懒惰关联时,我们应该以某种方式防止潜在的LazyInitializationExceptions。在处理懒惰关联的少数方法中,我们将在此列举其中的两种。

6.1. Entity Graphs

6.1.实体图

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

在Spring Data JPA中定义查询方法时,我们可以用@EntityGraph 来注释查询方法,以渴望获取实体的某些部分

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.

在这里,我们定义了一个临时的实体图来急切地加载permissions属性,尽管它默认是一个懒惰的集合。

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()

6.2.使用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:

有人可能会说,我们可以使用臭名昭著的Hibernate.initialize() 来获取我们需要的任何地方的懒惰关联,而不是使用实体图。

@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:

他们可能很聪明,还建议调用getPermissions()方法来触发获取过程。

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:

这两种方法都不值得推荐,因为它们会产生(至少)一个额外的查询,除了原来的查询之外,还要获取懒惰的关联。也就是说,Hibernate会产生以下查询来获取用户和他们的权限。

> 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:

另一方面,如果我们使用实体图,甚至是Fetch Joins,Hibernate将只需一次查询就能获取所有必要的数据。

> 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

7.结语

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.

在这篇文章中,我们将注意力转向Spring和其他一些企业框架中一个颇具争议的功能。视图中的开放会话。首先,我们从概念上和实现上了解了这个模式。然后,我们从生产力和性能的角度分析了它。

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

像往常一样,样本代码可在GitHub上获得。