Um guia para a sessão aberta do Spring à vista
1. Visão geral
Sessão por solicitação é um padrão transacional para vincular a sessão de persistência e solicitar ciclos de vida. Não surpreendentemente, o Spring vem com sua própria implementação desse padrão, chamadaOpenSessionInViewInterceptor, para facilitar o trabalho com associações lazy e, portanto, melhorar a produtividade do desenvolvedor.
Neste tutorial, primeiro, vamos aprender como o interceptor funciona internamente e, em seguida, veremos como esse padrãocontroversial pode ser uma faca de dois gumes para nossos aplicativos!
2. Introdução à sessão aberta no modo de exibição
Para entender melhor a função de Open Session in View (OSIV), vamos supor que temos uma solicitação de entrada:
-
O Spring abre um novo HibernateSession no início da solicitação. EssesSessions não estão necessariamente conectados ao banco de dados.
-
Cada vez que o aplicativo precisar de umSession, it irá reutilizar o já existente.
-
No final da solicitação, o mesmo interceptor fecha queSession.
À primeira vista, pode fazer sentido ativar esse recurso. Afinal, a estrutura lida com a criação e o encerramento da sessão, então os desenvolvedores não se preocupam com esses detalhes aparentemente de baixo nível. Isso, por sua vez, aumenta a produtividade do desenvolvedor.
No entanto, às vezes,OSIV can cause subtle performance issues in production. Geralmente, esses tipos de problemas são muito difíceis de diagnosticar.
2.1. Spring Boot
By default, OSIV is active in Spring Boot applications. Apesar disso, a partir do Spring Boot 2.0, ele nos avisa do fato de que é habilitado na inicialização do aplicativo se não o configuramos explicitamente:
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
De qualquer forma, podemos desabilitar o OSIV usando a propriedade de configuraçãospring.jpa.open-in-view:
spring.jpa.open-in-view=false
2.2. Padrão ou antipadrão?
Sempre houve reações contraditórias em relação ao OSIV. O principal argumento do campo pró-OSIV é a produtividade do desenvolvedor, especialmente ao lidar comlazy associations.
Por outro lado, os problemas de desempenho do banco de dados são o principal argumento da campanha anti-OSIV. Mais tarde, vamos avaliar os dois argumentos em detalhes.
3. Herói de inicialização preguiçosa
Como o OSIV vincula o ciclo de slifSession a cada solicitação,Hibernate can resolve lazy associations even after returning from an explicit@Transactional service.
Para entender melhor isso, vamos supor que estejamos modelando nossos usuários e suas permissões de segurança:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
@ElementCollection
private Set permissions;
// getters and setters
}
Semelhante a outros relacionamentos um para muitos e muitos para muitos, a propriedadepermissions é uma coleção preguiçosa.
Então, em nossa implementação de camada de serviço, vamos demarcar explicitamente nosso limite transacional usando@Transactional:
@Service
public class SimpleUserService implements UserService {
private final UserRepository userRepository;
public SimpleUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public Optional findOne(String username) {
return userRepository.findByUsername(username);
}
}
3.1. A expectativa
Aqui está o que esperamos que aconteça quando nosso código chamar o métodofindOne :
-
Inicialmente, o proxy Spring intercepta a chamada e obtém a transação atual ou cria uma, se não houver nenhuma.
-
Em seguida, delega a chamada de método para nossa implementação.
-
Finalmente, o proxy confirma a transação e, conseqüentemente, fecha oSession subjacente. Afinal, só precisamos desseSession de nossa camada de serviço.
Na implementação do métodofindOne method, não inicializamos a coleçãopermissions . Therefore, we shouldn’t be able to use the permissions after the method returns. Se iterarmos nesta propriedade, we deve obter umLazyInitializationException.
3.2. Bem-vindo ao mundo real
Vamos escrever um controlador REST simples para ver se podemos usar a propriedadepermissions:
@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());
}
}
Aqui, iteramos sobrepermissions durante entidade para conversão DTO. Como esperamos que a conversão falhe com umLazyInitializationException,, o seguinte teste não deve passar:
@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")));
}
}
No entanto, este teste não lança nenhuma exceção e é aprovado.
Como o OSIV cria umSession no início da solicitação, o proxy transacional ustenta oSession atualmente disponível em vez de criar um. totalmente novo
Portanto, apesar do que podemos esperar, podemos realmente usar apermissions property mesmo fora de uma@Transactional explícita. Além disso, esse tipo de associação lenta pode ser buscada em qualquer lugar do escopo de solicitação atual.
3.3. Produtividade do desenvolvedor
If OSIV wasn’t enabled, we’d have to manually initialize all necessary lazy associations in a transactional context. A maneira mais rudimentar (e geralmente errada) é usar o métodoHibernate.initialize() :
@Override
@Transactional(readOnly = true)
public Optional findOne(String username) {
Optional user = userRepository.findByUsername(username);
user.ifPresent(u -> Hibernate.initialize(u.getPermissions()));
return user;
}
Até agora, o efeito do OSIV na produtividade do desenvolvedor é óbvio. No entanto, nem sempre se trata da produtividade do desenvolvedor.
4. Vilão de desempenho
Suponha que tenhamos que estender nosso serviço de usuário simples paracall another remote service after fetching the user from the database:
@Override
public Optional findOne(String username) {
Optional user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
Aqui, estamos removendo a anotação@Transactional an, já que claramente não queremos manter oSession conectado esperando pelo serviço remoto.
4.1. Evitando E / S Mistas
Vamos esclarecer o que acontece se não removermos a anotação@Transactional . Suppose the new remote service is responding a little more slowly than usual:
-
A princípio, o proxy Spring obtém oSession atual ou cria um novo. De qualquer forma, esteSession ainda não está conectado. Ou seja, não está usando nenhuma conexão do pool.
-
Depois de executar a consulta para encontrar um usuário, oSession torna-se conectado e pega emprestado umConnection do pool.
-
Se todo o método for transacional, o método continua a chamar o serviço remoto lento, mantendo osConnection emprestados.
Imagine that during this period, we get a burst of calls to the findOne method. Então, depois de um tempo, todos osConnections podem esperar por uma resposta dessa chamada de API. Portanto,we may soon run out of database connections.
A mistura de E / S de banco de dados com outros tipos de E / S em um contexto transacional é um mau cheiro, e devemos evitá-lo a todo custo.
De qualquer forma,since we removed the @Transactional annotation from our service, we’re expecting to be safe.
4.2. Esgotando o pool de conexão
When OSIV is active, there is always a Session in the current request scope, mesmo se removermos@Transactional. Embora esteSession não esteja conectado inicialmente, após nosso primeiro IO de banco de dados, ele se conecta e permanece assim até o final da solicitação.
Portanto, nossa implementação de serviço de aparência inocente e recentemente otimizada é uma receita para um desastre na presença do OSIV:
@Override
public Optional findOne(String username) {
Optional user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
Aqui está o que acontece enquanto o OSIV está ativado:
-
No início da solicitação, o filtro correspondente cria um novoSession.
-
Quando chamamos o métodofindByUsername , esseSession borrows aConnection da piscina.
-
OSession permanece conectado até o final da solicitação.
Mesmo que esperemos que nosso código de serviço não esgote o pool de conexão, a mera presença de OSIV pode fazer com que todo o aplicativo pare de responder.
Para piorar ainda mais as coisas,the root cause of the problem (slow remote service) and the symptom (database connection pool) are unrelated. Devido a essa pouca correlação, é difícil diagnosticar esses problemas de desempenho em ambientes de produção.
4.3. Consultas desnecessárias
Infelizmente, esgotar o pool de conexões não é o único problema de desempenho relacionado ao OSIV.
ComoSession está aberto para todo o ciclo de vida da solicitação,some property navigations may trigger a few more unwanted queries outside of the transactional context. É até possível acabar comn+1 select problem, e a pior notícia é que podemos não perceber isso até a produção.
Adicionando insulto à injúria, oSession executes all those extra queries in auto-commit mode. No modo de confirmação automática, cada instrução SQL é tratada como uma transação e é confirmada automaticamente logo após ser executada. Isso, por sua vez, coloca muita pressão no banco de dados.
5. Escolha sabiamente
Se o OSIV é um padrão ou um antipadrão é irrelevante. O mais importante aqui é a realidade em que vivemos.
If we’re developing a simple CRUD service, it might make sense to use the OSIV, pois podemos nunca encontrar esses problemas de desempenho.
Por outro lado,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.
Em caso de dúvida, comece sem o OSIV, pois podemos ativá-lo facilmente mais tarde. Por outro lado, desabilitar um OSIV já habilitado pode ser complicado, pois podemos precisar lidar com muitosLazyInitializationExceptions.
O ponto principal é que devemos estar cientes das compensações ao usar ou ignorar o OSIV.
6. Alternativas
Se desabilitarmos o OSIV, devemos evitar de alguma forma o potencialLazyInitializationExceptions ao lidar com associações preguiçosas. Entre um punhado de abordagens para lidar com associações preguiçosas, vamos enumerar duas delas aqui.
6.1. Gráficos de entidades
Ao definir métodos de consulta no Spring Data JPA, podemos anotar um método de consulta com@EntityGraph toeagerly fetch some part of the entity:
public interface UserRepository extends JpaRepository {
@EntityGraph(attributePaths = "permissions")
Optional findByUsername(String username);
}
Aqui, estamos definindo um gráfico de entidade ad-hoc para carregar opermissions attribute avidamente, embora seja uma coleção preguiçosa por padrão.
Se precisarmos retornar várias projeções da mesma consulta, devemos definir várias consultas com configurações diferentes de gráfico de entidade:
public interface UserRepository extends JpaRepository {
@EntityGraph(attributePaths = "permissions")
Optional findDetailedByUsername(String username);
Optional findSummaryByUsername(String username);
}
6.2. Advertências ao usarHibernate.initialize()
Alguém pode argumentar que, em vez de usar gráficos de entidade, podemos usar o notórioHibernate.initialize() to buscar associações lazy sempre que precisarmos fazer isso:
@Override
@Transactional(readOnly = true)
public Optional findOne(String username) {
Optional user = userRepository.findByUsername(username);
user.ifPresent(u -> Hibernate.initialize(u.getPermissions()));
return user;
}
Eles podem ser espertos sobre isso e também sugerir chamar o métodogetPermissions() para acionar o processo de busca:
Optional user = userRepository.findByUsername(username);
user.ifPresent(u -> {
Set permissions = u.getPermissions();
System.out.println("Permissions loaded: " + permissions.size());
});
Ambas as abordagens não são recomendadas desdethey incur (at least) one extra query, além da original, para buscar a associação preguiçosa. Ou seja, o Hibernate gera as seguintes consultas para buscar usuários e suas permissões:
> 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=?
Embora a maioria dos bancos de dados seja muito boa para executar a segunda consulta, devemos evitar essa ida e volta extra à rede.
Por outro lado, se usarmos gráficos de entidade ou mesmoFetch Joins, o Hibernate irá buscar todos os dados necessários com apenas uma consulta:
> 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. Conclusão
Neste artigo, voltamos nossa atenção para um recurso bastante controverso no Spring e em algumas outras estruturas empresariais: Open Session in View. Primeiro, nos tornamos aquáticos com esse padrão tanto conceitualmente quanto em termos de implementação. Em seguida, analisamos a perspectiva de produtividade e desempenho.
Como de costume, o código de amostra está disponívelover on GitHub.