每个请求一个会话是一种将持久性会话和请求生命周期联系在一起的事务模式。 不出所料, Spring 自带这种模式的实现,名为 OpenSessionInViewInterceptor, 方便使用惰性关联,从而提高开发人员的工作效率.
在本教程中,首先,我们将学习拦截器如何在内部工作,然后,我们将看到这个有争议的模式如何成为我们的应用程序的一把双刃剑!
为了更好地理解 Open Session in View (OSIV) 的作用,假设我们有一个传入请求:
乍一看,启用此功能可能很有意义。 毕竟,框架负责处理会话的创建和终止,因此开发人员不会关心这些看似低级的细节。 这反过来又提高了开发人员的生产力。
然而,有时, 在生产中,OSIV可能会导致细微的性能问题. 通常,这类问题是很难诊断。
默认情况下,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
无论如何,我们可以使用 spring.jpa.open-in-view 配置属性禁用 OSIV:
spring.jpa.open-in-view=false
人们对OSIV的反应总是褒贬不一. 支持OSIV阵营的主要论点是开发人员的生产力,特别是在处理懒惰关联时.
另一方面,数据库性能问题是反 OSIV 活动的主要论点。 稍后,我们将详细评估这两个论点。
由于 OSIV 将 Session 生命周期绑定到每个请求, 即使从显式 @Transactional
服务返回后,Hibernate 也可以解决惰性关联.
为了更好地理解这一点,假设我们正在对用户及其安全权限进行建模:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
@ElementCollection
private Set<String> permissions;
// getters and setters
}
WS注释:
@ElementCollection
主要用于映射非实体(可embedded或基本数据类型),而@OneToMany
用于映射实体.
与其他一对多和多对多关系类似,permissions 属性是一个惰性集合.
然后,在我们的服务层实现中,让我们使用 @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);
}
}
当我们的代码调用 findOne 方法时,我们期望发生以下情况:
在findOne方法实现中,我们没有初始化permissions集合. 因此,我们不应该在方法返回后使用 permissions。 如果我们对这个属性进行迭代,我们应该得到一个 LazyInitializationException.
让我们编写一个简单的 REST 控制器来看看我们是否可以使用 permissions 属性:
@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());
}
}
在这里,我们在实体到 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")));
}
}
但是,此测试不会引发任何异常,并且通过了。
因为OSIV在请求开始时创建了一个Session,事务代理使用当前可用的Session,而不是创建一个全新的。
因此,尽管我们可能会期望,我们实际上甚至可以在显式 @Transactional 之外使用 permissions 属性。 此外,可以在当前请求范围内的任何位置获取这些类型的惰性关联。
如果未启用 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;
}
到目前为止,OSIV 对开发人员生产力的影响是显而易见的。 但是,这并不总是与开发人员的生产力有关。
假设我们必须扩展我们的简单用户服务以在从数据库中获取用户后调用另一个远程服务:
@Override
public Optional<User> findOne(String username) {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
在这里,我们删除了 @Transactional 注释,因为我们显然不想在等待远程服务时保持连接的 Session。
让我们澄清如果我们不删除 @Transactional 注释会发生什么。 假设新的远程服务的响应速度比平时慢一点:
想象一下,在此期间,我们收到了对 findOne 方法的大量调用。 然后,一段时间后,所有 Connections 可能会等待来自该 API 调用的响应。 因此,我们可能很快就会耗尽数据库连接。
在事务环境中,将数据库IOs与其他类型的IOs混合是一种糟糕的感觉,我们应该不惜一切代价避免这种情况。
无论如何,因为我们从服务中删除了*@Transactional*注释,所以我们希望它是安全的。
当OSIV处于活动状态时,在当前的请求范围内总是有一个Session, 即使我们删除了*@Transactional*。虽然这个Session最初没有连接,但在我们的第一个数据库IO之后,它被连接并一直保持到请求结束。
因此,我们看似无辜且最近优化的服务实现是 OSIV 存在的灾难的秘诀:
@Override
public Optional<User> findOne(String username) {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
以下是启用 OSIV 时发生的情况:
尽管我们希望服务代码不会耗尽连接池,但仅仅是OSIV的存在就可能使整个应用程序失去响应。
更糟糕的是,问题的根本原因(远程服务慢)和症状(数据库连接池)是不相关的。由于这种相关性很小,因此在生产环境中很难诊断这种性能问题。
不幸的是,耗尽连接池并不是唯一与 OSIV 相关的性能问题。
由于 Session 对整个请求生命周期都是开放的,因此某些属性导航可能会在事务上下文之外触发更多不需要的查询。 甚至有可能最终出现 n+1查询问题,而最糟糕的消息是,我们可能直到生产时才会注意到这一点。
更糟糕的是,Session以自动提交模式的方式执行所有这些额外的查询。在自动提交模式下,每个SQL语句都被视为一个事务,并在执行后立即自动提交。这反过来又给数据库带来了很大的压力。
OSIV 是模式还是反模式无关紧要。 关键要看我们所处的环境。
如果我们正在开发一个简单的CRUD服务,使用OSIV可能是有意义的,因为我们可能永远不会遇到那些性能问题。
另一方面,如果我们发现自己调用了很多远程服务,或者在事务上下文之外发生了很多事情,我们强烈建议完全禁用OSIV。
当有疑问时,不要使用OSIV,因为我们可以很容易地在以后启用它。另一方面,禁用一个已经启用的OSIV可能会很麻烦,因为我们可能需要处理大量的lazyinitializationexception
底线是,我们使用或忽略OSIV时应该注意的取舍。
如果我们禁用 OSIV,那么在处理惰性关联时,我们应该以某种方式防止潜在的 LazyInitializationExceptions。 在处理惰性关联的少数方法中,我们将在这里列举其中的两种。
JPA 2.1推出来的@EntityGraph
、 @NamedEntityGraph
用来提高查询效率, 很好地解决了N+1
条SQL的问题。 两者需要配合起来使用, 缺一不可。 @NamedEntityGraph配置在@Entity
上面, 而@EntityGraph配置在Repository的查询方法上面。
在 Spring Data JPA 中定义查询方法时,我们可以使用 @EntityGraph 注释查询方法以急切地获取实体的某些部分:
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "permissions")
Optional<User> findByUsername(String username);
}
在这里,我们定义了一个临时实体图来急切地加载 permissions 属性,即使它默认是一个惰性集合。
如果我们需要从同一个查询返回多个投影,那么我们应该定义多个具有不同实体图配置的查询:
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "permissions")
Optional<User> findDetailedByUsername(String username);
Optional<User> findSummaryByUsername(String username);
}
有人可能会争辩说,我们可以使用臭名昭著的 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;
}
他们可能很聪明,还建议调用 getPermissions() 方法来触发获取过程:
Optional<User> user = userRepository.findByUsername(username);
user.ifPresent(u -> {
Set<String> permissions = u.getPermissions();
System.out.println("Permissions loaded: " + permissions.size());
});
不推荐这两种方法,因为 除了原始查询之外,它们(至少)会产生一个额外的查询来获取惰性关联。 也就是说,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=?
尽管大多数数据库都非常擅长执行第二个查询,但我们应该避免额外的网络往返。
另一方面,如果我们使用实体图甚至 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=?
在本文中,我们将注意力转向 Spring 和其他一些企业框架中一个颇具争议的特性:在视图中打开会话(Open Session in View)。 首先,我们在概念上和实现上都熟悉了这种模式。 然后我们从生产力和性能的角度对其进行了分析。
和往常一样,示例代码在GitHub上可用.