Ein Leitfaden zur offenen Sitzung von Spring In View

1. Übersicht

Sitzung pro Anforderung ist ein Transaktionsmuster, um die Persistenzsitzung zu verknüpfen und Lebenszyklen anzufordern. Es überrascht nicht, dass Spring eine eigene Implementierung dieses Musters mit dem Namen OpenSessionInViewInterceptor enthält , um die Arbeit mit faulen Assoziationen zu erleichtern und damit die Entwicklerproduktivität zu verbessern.

In diesem Tutorial lernen wir zunächst, wie der Abfangjäger intern funktioniert, und dann sehen wir, wie dieses kontroverse Muster ein zweischneidiges Schwert für unsere Anwendungen sein kann!

2. Einführung in Open Session in View

Nehmen wir an, wir haben eine eingehende Anfrage, um die Rolle von Open Session in View (OSIV) besser zu verstehen:

  1. Frühling eröffnet eine neue Hibernate Session am Anfang der Anforderung. Diese Sitzungen sind nicht unbedingt mit der Datenbank verbunden.
  2. Jedes Mal, wenn die Anwendung eine Sitzung benötigt, wird die bereits vorhandene wiederverwendet.
  3. Am Ende der Anforderung schließt derselbe Interceptor diese Sitzung.

Auf den ersten Blick kann es sinnvoll sein, diese Funktion zu aktivieren. Schließlich übernimmt das Framework die Erstellung und Beendigung von Sitzungen, sodass sich die Entwickler nicht mit diesen scheinbar einfachen Details befassen. Dies wiederum steigert die Entwicklerproduktivität.

Manchmal kann OSIV jedoch subtile Leistungsprobleme in der Produktion verursachen . Normalerweise sind diese Probleme sehr schwer zu diagnostizieren.

2.1. Frühlingsstiefel

Standardmäßig ist OSIV in Spring Boot-Anwendungen aktiv . Trotzdem warnt uns Spring Spring 2.0 davor, dass es beim Start der Anwendung aktiviert wird, wenn wir es nicht explizit konfiguriert haben:

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

Auf jeden Fall können wir das OSIV mithilfe der Konfigurationseigenschaft spring.jpa.open-in-view deaktivieren :

spring.jpa.open-in-view=false

2.2. Muster oder Anti-Muster?

Es gab immer gemischte Reaktionen gegenüber OSIV. Das Hauptargument des Pro-OSIV-Lagers ist die Entwicklerproduktivität, insbesondere im Umgang mit faulen Assoziationen.

Andererseits sind Probleme mit der Datenbankleistung das Hauptargument der Anti-OSIV-Kampagne. Später werden wir beide Argumente im Detail bewerten.

3. Lazy Initialization Hero

Da OSIV den Sitzungslebenszyklus an jede Anforderung bindet , kann Hibernate verzögerte Zuordnungen auch nach der Rückkehr von einem expliziten @ Transactional- Dienst auflösen .

Nehmen wir zum besseren Verständnis an, wir modellieren unsere Benutzer und ihre Sicherheitsberechtigungen:

@Entity @Table(name = "users") public class User { @Id @GeneratedValue private Long id; private String username; @ElementCollection private Set permissions; // getters and setters }

Ähnlich wie bei anderen Eins-zu-Viele- und Viele-zu-Viele-Beziehungen ist die Berechtigungseigenschaft eine verzögerte Sammlung.

Lassen Sie uns dann in unserer Service-Layer-Implementierung unsere Transaktionsgrenze mithilfe von @Transactional explizit abgrenzen :

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

Folgendes erwarten wir, wenn unser Code die findOne- Methode aufruft :

  1. Zunächst fängt der Spring-Proxy den Anruf ab und ruft die aktuelle Transaktion ab oder erstellt eine, wenn keine vorhanden ist.
  2. Anschließend delegiert es den Methodenaufruf an unsere Implementierung.
  3. Schließlich schreibt der Proxy die Transaktion fest und schließt folglich die zugrunde liegende Sitzung . Schließlich benötigen wir diese Sitzung nur in unserer Service-Schicht.

In der Implementierung der findOne- Methode haben wir die Berechtigungssammlung nicht initialisiert . Daher sollten wir die Berechtigungen nach der Rückkehr der Methode nicht verwenden können . Wenn wir diese Eigenschaft iterieren , sollten wir eine LazyInitializationException erhalten.

3.2. Willkommen in der realen Welt

Schreiben wir einen einfachen REST-Controller, um zu sehen, ob wir die Eigenschaft permissions verwenden können :

@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()); } }

Hier iterieren wir über Berechtigungen während der Konvertierung von Entität in DTO. Da wir erwarten, dass diese Konvertierung mit einer LazyInitializationException fehlschlägt, sollte der folgende Test nicht bestanden werden:

@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"))); } }

Dieser Test wirft jedoch keine Ausnahmen und ist erfolgreich.

Da OSIV zu Beginn der Anforderung eine Sitzung erstellt , wird der Transaktionsproxyverwendet die aktuell verfügbare Sitzung, anstatt eine brandneue zu erstellen .

Trotz aller Erwartungen können wir die Eigenschaft permissions sogar außerhalb eines expliziten @Transactional verwenden . Darüber hinaus können diese Arten von faulen Zuordnungen überall im aktuellen Anforderungsbereich abgerufen werden.

3.3. Zur Entwicklerproduktivität

Wenn OSIV nicht aktiviert wäre, müssten wir alle erforderlichen Lazy-Assoziationen in einem Transaktionskontext manuell initialisieren . Der rudimentärste (und normalerweise falsche) Weg ist die Verwendung der Hibernate.initialize () -Methode:

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

Inzwischen ist die Auswirkung von OSIV auf die Entwicklerproduktivität offensichtlich. Es geht jedoch nicht immer um Entwicklerproduktivität.

4. Leistungsschurke

Suppose we have to extend our simple user service to call 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; }

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 active, there 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 findOne(String username) { Optional 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 { @EntityGraph(attributePaths = "permissions") Optional 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 { @EntityGraph(attributePaths = "permissions") Optional findDetailedByUsername(String username); Optional 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 findOne(String username) { Optional 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 = userRepository.findByUsername(username); user.ifPresent(u -> { Set 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. Fazit

In diesem Artikel haben wir unsere Aufmerksamkeit auf ein ziemlich kontroverses Feature im Frühjahr und einige andere Unternehmens-Frameworks gerichtet: Open Session in View. Zunächst wurden wir sowohl konzeptionell als auch implementierungsmäßig mit diesem Muster aquatintiert. Dann haben wir es unter Produktivitäts- und Leistungsgesichtspunkten analysiert.

Wie üblich ist der Beispielcode auf GitHub verfügbar.