Een gids voor de open sessie van Spring in zicht

1. Overzicht

Sessie per verzoek is een transactiepatroon om de persistentiesessie te verbinden en levenscycli aan te vragen. Het is niet verrassend dat Spring wordt geleverd met een eigen implementatie van dit patroon, genaamd OpenSessionInViewInterceptor, om het werken met luie verenigingen te vergemakkelijken en daardoor de productiviteit van ontwikkelaars te verbeteren.

In deze tutorial gaan we eerst leren hoe de interceptor intern werkt, en dan zullen we zien hoe dit controversiële patroon een tweesnijdend zwaard kan zijn voor onze toepassingen!

2. Introductie van Open Session in View

Laten we aannemen dat we een inkomend verzoek hebben om de rol van Open Session in View (OSIV) beter te begrijpen:

  1. De lente opent een nieuwe winterslaap Sessie aan het begin van het verzoek. Deze Sessies zijn niet noodzakelijkerwijs verbonden met de database.
  2. Elke keer dat de applicatie een Sessie, het zal de reeds bestaande hergebruiken.
  3. Aan het einde van het verzoek sluit dezelfde interceptor dat Sessie.

Op het eerste gezicht kan het zinvol zijn om deze functie in te schakelen. Het framework zorgt immers voor het maken en beëindigen van de sessie, zodat de ontwikkelaars zich niet bezighouden met deze ogenschijnlijk lage details. Dit verhoogt op zijn beurt de productiviteit van ontwikkelaars.

Soms OSIV kan tijdens de productie subtiele prestatieproblemen veroorzaken. Meestal zijn dit soort problemen erg moeilijk te diagnosticeren.

2.1. Spring Boot

OSIV is standaard actief in Spring Boot-applicaties. Desondanks waarschuwt het ons vanaf Spring Boot 2.0 voor het feit dat het is ingeschakeld bij het opstarten van de applicatie als we het niet expliciet hebben geconfigureerd:

spring.jpa.open-in-view is standaard ingeschakeld. Daarom kunnen databasequery's worden uitgevoerd tijdens weergave van weergaven. Configureer spring.jpa.open-in-view expliciet om deze waarschuwing uit te schakelen

Hoe dan ook, we kunnen de OSIV uitschakelen door de spring.jpa.open-in-view configuratie-eigenschap:

spring.jpa.open-in-view = false

2.2. Patroon of antipatroon?

Er zijn altijd gemengde reacties geweest op OSIV. Het belangrijkste argument van het pro-OSIV-kamp is de productiviteit van ontwikkelaars, vooral als het gaat om luie verenigingen.

Aan de andere kant zijn problemen met de databaseprestaties het belangrijkste argument van de anti-OSIV-campagne. Later gaan we beide argumenten in detail beoordelen.

3. Luie initialisatieheld

Omdat OSIV het Sessie levenscyclus voor elk verzoek, Hibernate kan luie associaties oplossen, zelfs na terugkeer van een expliciete @Transactional onderhoud.

Laten we, om dit beter te begrijpen, aannemen dat we onze gebruikers en hun beveiligingsrechten modelleren:

@Entity @Table (name = "gebruikers") openbare klasse Gebruiker {@Id @GeneratedValue privé Lange id; private String gebruikersnaam; @ElementCollection private Stel machtigingen in; // getters en setters}

Net als bij andere een-op-veel- en veel-op-veel-relaties, is de rechten eigenschap is een luie verzameling.

Laten we vervolgens in onze servicelaagimplementatie expliciet onze transactiegrens afbakenen met behulp van @Transactional:

@Service openbare klasse SimpleUserService implementeert UserService {private final UserRepository userRepository; openbare SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @Override @Transactional (readOnly = true) publiek Optioneel findOne (String gebruikersnaam) {return userRepository.findByUsername (gebruikersnaam); }}

3.1. De verwachting

Dit is wat we verwachten dat er zal gebeuren wanneer onze code het vind een methode:

  1. In eerste instantie onderschept de Spring-proxy de oproep en ontvangt de huidige transactie of maakt er een als er geen bestaat.
  2. Vervolgens delegeert het de methodeaanroep naar onze implementatie.
  3. Ten slotte verbindt de gevolmachtigde de transactie en sluit bijgevolg de onderliggende waarde Sessie. Dat hebben we tenslotte alleen nodig Sessie in onze servicelaag.

In de vind een methode implementatie, hebben we de rechten verzameling. Daarom zouden we de rechten na de methode keert terug. Als we deze eigenschap herhalen, we zouden een LazyInitializationException.

3.2. Welkom in de echte wereld

Laten we een eenvoudige REST-controller schrijven om te zien of we de rechten eigendom:

@RestController @RequestMapping ("/ gebruikers") openbare klasse UserController {privé laatste UserService userService; openbare UserController (UserService userService) {this.userService = userService; } @GetMapping ("/ {gebruikersnaam}") openbare ResponseEntity findOne (@PathVariable String gebruikersnaam) {return userService .findOne (gebruikersnaam) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound ().bouwen()); }}

Hier herhalen we rechten tijdens de conversie van entiteit naar DTO. Omdat we verwachten dat die conversie mislukt met een LazyInitializationException, de volgende test mag niet slagen:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles ("test") klasse UserControllerIntegrationTest {@Autowired privé UserRepository userRepository; @Autowired privé MockMvc mockMvc; @BeforeEach void setUp () {Gebruiker gebruiker = nieuwe gebruiker (); user.setUsername ("root"); user.setPermissions (nieuwe HashSet (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (gebruiker); } @Test ongeldig gegevenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () gooit uitzondering {mockMvc.perform (get ("/ gebruikers / root")) .andExpect (status (). IsOk ()) .andExpect (jsonPath ("$. Gebruikersnaam"). Waarde ("$. Gebruikersnaam"). Waarde ("$. Gebruikersnaam"). Waarde ("$. Gebruikersnaam"). root ")) .andExpect (jsonPath (" $. permissions ", containsInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

Deze test levert echter geen uitzonderingen op en slaagt.

Omdat OSIV een Sessie aan het begin van het verzoek, de transactionele proxymaakt gebruik van de huidige beschikbare Sessie in plaats van een geheel nieuwe te maken.

Dus, ondanks wat we zouden verwachten, kunnen we de rechten eigenschap zelfs buiten een expliciete @Transactional. Bovendien kunnen dit soort luie associaties overal in het huidige aanvraagbereik worden opgehaald.

3.3. Over de productiviteit van ontwikkelaars

Als OSIV niet was ingeschakeld, zouden we alle noodzakelijke luie associaties handmatig moeten initialiseren in een transactionele context. De meest rudimentaire (en meestal verkeerde) manier is om de Slaapstand.initialize () methode:

@Override @Transactional (readOnly = true) public Optioneel findOne (String gebruikersnaam) {Optioneel user = userRepository.findByUsername (gebruikersnaam); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); terugkeer gebruiker; }

Het effect van OSIV op de productiviteit van ontwikkelaars is inmiddels duidelijk. Het gaat echter niet altijd om de productiviteit van ontwikkelaars.

4. Prestatieschurk

Stel dat we onze eenvoudige gebruikersservice moeten uitbreiden naar bel een andere externe service nadat u de gebruiker uit de database hebt opgehaald:

@Override public Optioneel findOne (String gebruikersnaam) {Optioneel user = userRepository.findByUsername (gebruikersnaam); if (user.isPresent ()) {// externe oproep} terugkeer gebruiker; }

Hier verwijderen we het @Transactional annotatie omdat we de verbinding duidelijk niet willen houden Sessie in afwachting van de service op afstand.

4.1. Gemengde IO's vermijden

Laten we verduidelijken wat er gebeurt als we het @Transactional annotatie. Stel dat de nieuwe service op afstand iets langzamer reageert dan normaal:

  1. In eerste instantie krijgt de Spring-proxy de huidige Sessie of maakt een nieuwe aan. Hoe dan ook, dit Sessie is nog niet verbonden. Dat wil zeggen, het gebruikt geen enkele verbinding vanuit de pool.
  2. Zodra we de zoekopdracht hebben uitgevoerd om een ​​gebruiker te vinden, wordt het Sessie wordt verbonden en leent een Verbinding van het zwembad.
  3. Als de hele methode transactiegerelateerd is, gaat de methode verder met het aanroepen van de langzame externe service terwijl de geleende wordt behouden Verbinding.

Stel je voor dat we tijdens deze periode een reeks telefoontjes krijgen naar het vind een methode. Toen, na een tijdje, allemaal Verbindingen kan wachten op een reactie van die API-aanroep. Daarom we kunnen binnenkort geen databaseverbindingen meer hebben.

Het combineren van database-IO's met andere soorten IO's in een transactionele context is een slechte geur en we moeten dit koste wat het kost vermijden.

In ieder geval, sinds we de @Transactional annotatie van onze service, verwachten we veilig te zijn.

4.2. De verbindingspool uitputten

Als OSIV actief is, er is altijd een Sessie in het huidige aanvraagbereik, zelfs als we verwijderen @Transactional. Hoewel dit Sessie is aanvankelijk niet verbonden, na onze eerste database-IO wordt het verbonden en blijft dit zo tot het einde van het verzoek.

Onze onschuldig ogende en recent geoptimaliseerde service-implementatie is dus een recept voor een ramp in de aanwezigheid van OSIV:

@Override public Optioneel findOne (String gebruikersnaam) {Optioneel user = userRepository.findByUsername (gebruikersnaam); if (user.isPresent ()) {// externe oproep} terugkeer gebruiker; }

Dit is wat er gebeurt terwijl de OSIV is ingeschakeld:

  1. Aan het begin van het verzoek maakt het bijbehorende filter een nieuw Sessie.
  2. Als we de findByUsername methode, dat Sessie leent een Verbinding van het zwembad.
  3. De Sessie blijft verbonden tot het einde van het verzoek.

Ook al verwachten we dat onze servicecode de verbindingspool niet zal uitputten, alleen al de aanwezigheid van OSIV kan ervoor zorgen dat de hele applicatie niet meer reageert.

Om het nog erger te maken, de hoofdoorzaak van het probleem (trage service op afstand) en het symptoom (pool van databaseverbindingen) staan ​​los van elkaar. Vanwege deze kleine correlatie zijn dergelijke prestatieproblemen moeilijk te diagnosticeren in productieomgevingen.

4.3. Onnodige vragen

Helaas is het uitputten van de verbindingspool niet het enige OSIV-gerelateerde prestatieprobleem.

Sinds de Sessie staat open voor de hele levenscyclus van het verzoek, sommige property-navigaties kunnen buiten de transactionele context nog een paar ongewenste zoekopdrachten activeren. Het is zelfs mogelijk om een ​​n + 1 selectieprobleem te krijgen, en het slechtste nieuws is dat we dit pas in de productie zullen opmerken.

Om nog erger te maken, de Sessie voert al die extra queries uit in auto-commit mode. In de modus voor automatisch vastleggen wordt elke SQL-instructie behandeld als een transactie en automatisch vastgelegd direct nadat deze is uitgevoerd. Dit legt op zijn beurt een grote druk op de database.

5. Kies verstandig

Of de OSIV een patroon of een antipatroon is, doet er niet toe. Het belangrijkste hier is de realiteit waarin we leven.

Als we een eenvoudige CRUD-service ontwikkelen, kan het zinvol zijn om de OSIV te gebruiken, aangezien we die prestatieproblemen misschien nooit zullen tegenkomen.

Aan de andere kant, als we merken dat we veel externe services bellen of als er zoveel gebeurt buiten onze transactionele contexten, is het sterk aanbevolen om de OSIV helemaal uit te schakelen.

Begin bij twijfel zonder OSIV, aangezien we het later gemakkelijk kunnen inschakelen. Aan de andere kant kan het omslachtig zijn om een ​​reeds ingeschakelde OSIV uit te schakelen, omdat we er mogelijk veel van moeten afhandelen LazyInitializationExceptions.

Het komt erop neer dat we ons bewust moeten zijn van de afwegingen bij het gebruik of negeren van OSIV.

6. Alternatieven

Als we OSIV uitschakelen, moeten we op de een of andere manier potentieel voorkomen LazyInitializationExceptions als het om luie associaties gaat. Onder een handvol benaderingen om met luie associaties om te gaan, zullen we er hier twee opsommen.

6.1. Entiteitsgrafieken

Bij het definiëren van query-methoden in Spring Data JPA, kunnen we een query-methode annoteren met @EntityGraph gretig een deel van de entiteit ophalen:

openbare interface UserRepository breidt JpaRepository {@EntityGraph (attributePaths = "permissions") Optioneel findByUsername (String gebruikersnaam) uit; }

Hier definiëren we een ad-hoc entiteitsgrafiek om het rechten attribuut gretig toe, ook al is het standaard een luie verzameling.

Als we meerdere projecties van dezelfde query moeten retourneren, moeten we meerdere query's definiëren met verschillende entiteitgrafiekconfiguraties:

openbare interface UserRepository breidt JpaRepository {@EntityGraph (attributePaths = "permissions") Optioneel findDetailedByUsername (String gebruikersnaam) uit; Optioneel findSummaryByUsername (String gebruikersnaam); }

6.2. Waarschuwingen bij gebruik Slaapstand.initialize ()

Je zou kunnen zeggen dat we in plaats van entiteitsgrafieken het beruchte te gebruiken Slaapstand.initialize () om luie associaties op te halen waar dat nodig is:

@Override @Transactional (readOnly = true) public Optioneel findOne (String gebruikersnaam) {Optioneel user = userRepository.findByUsername (gebruikersnaam); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); terugkeer gebruiker; }

Ze zijn er misschien slim in en stellen ook voor om het getPermissions () methode om het ophaalproces te activeren:

Optioneel user = userRepository.findByUsername (gebruikersnaam); user.ifPresent (u -> {Set permissions = u.getPermissions (); System.out.println ("Permissions geladen:" + permissions.size ());});

Beide benaderingen worden sindsdien niet aanbevolen ze maken (minstens) één extra vraag, naast de originele, om de luie associatie op te halen. Dat wil zeggen, Hibernate genereert de volgende zoekopdrachten om gebruikers en hun machtigingen op te halen:

> selecteer u.id, u.username van gebruikers u waarbij u.username =? > selecteer p.user_id, p.permissions van user_permissions p waarbij p.user_id =? 

Hoewel de meeste databases redelijk goed zijn in het uitvoeren van de tweede query, moeten we die extra netwerkronde vermijden.

Aan de andere kant, als we entiteitsgrafieken of zelfs Fetch Joins gebruiken, haalt Hibernate alle benodigde gegevens op met slechts één query:

> selecteer u.id, u.username, p.user_id, p.permissions van gebruikers u linksbuiten join user_permissions p op u.id = p.user_id waar u.username =?

7. Conclusie

In dit artikel hebben we onze aandacht gericht op een behoorlijk controversiële functie in Spring en een paar andere enterprise-frameworks: Open Session in View. Ten eerste hebben we dit patroon zowel conceptueel als implementatiegewijs leren kennen. Vervolgens hebben we het geanalyseerd vanuit het oogpunt van productiviteit en prestatie.

Zoals gewoonlijk is de voorbeeldcode beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found