En guide til forårets åbne session i udsigt

1. Oversigt

Session pr. Anmodning er et transaktionsmønster til at binde udholdenhedssessionen og anmode om livscyklusser sammen. Ikke overraskende kommer Spring med sin egen implementering af dette mønster, navngivet OpenSessionInViewInterceptor, for at lette arbejdet med dovne foreninger og dermed forbedre udviklerens produktivitet.

I denne vejledning skal vi først lære, hvordan interceptor fungerer internt, og så vil vi se, hvordan dette kontroversielle mønster kan være et tveægget sværd til vores applikationer!

2. Introduktion til Open Session in View

For bedre at forstå rollen som Open Session in View (OSIV), lad os antage, at vi har en indgående anmodning:

  1. Foråret åbner et nyt dvale Session i begyndelsen af ​​anmodningen. Disse Sessioner er ikke nødvendigvis forbundet til databasen.
  2. Hver gang applikationen har brug for en Session, det vil genbruge den allerede eksisterende.
  3. I slutningen af ​​anmodningen lukker den samme interceptor det Session.

Ved første øjekast kan det give mening at aktivere denne funktion. Når alt kommer til alt, håndterer rammen sessionens oprettelse og afslutning, så udviklerne ikke bekymrer sig om disse tilsyneladende lave detaljer. Dette øger igen udviklerens produktivitet.

Men nogle gange OSIV kan forårsage subtile ydeevne problemer i produktionen. Normalt er disse typer problemer meget svære at diagnosticere.

2.1. Spring Boot

OSIV er som standard aktiv i Spring Boot-applikationer. På trods af dette advarer det os fra Spring Boot 2.0 om, at det er aktiveret ved opstart af applikationen, hvis vi ikke har konfigureret det eksplicit:

spring.jpa.open-in-view er aktiveret som standard. Derfor kan databaseforespørgsler udføres under visningsgengivelse. Konfigurer eksplicit spring.jpa.open-in-view for at deaktivere denne advarsel

Under alle omstændigheder kan vi deaktivere OSIV ved hjælp af spring.jpa.open-in-view konfigurationsegenskab:

spring.jpa.open-in-view = false

2.2. Mønster eller antimønster?

Der har altid været blandede reaktioner over for OSIV. Hovedargumentet i pro-OSIV-lejren er udviklerens produktivitet, især når det drejer sig om dovne foreninger.

På den anden side er problemer med databaseydelse det primære argument for anti-OSIV-kampagnen. Senere vil vi vurdere begge argumenter i detaljer.

3. Lazy Initialization Hero

Siden OSIV binder Session livscyklus til hver anmodning, Dvaletilstand kan løse dovne foreninger, selv efter at de vender tilbage fra en eksplicit @Transaktionel service.

For bedre at forstå dette, lad os antage, at vi modellerer vores brugere og deres sikkerhedstilladelser:

@Entity @Table (name = "brugere") offentlig klasse bruger {@Id @GeneratedValue privat Lang id; privat streng brugernavn; @ElementCollection private Set tilladelser; // getters og setters}

Svarende til andre en-til-mange og mange-til-mange forhold, tilladelser ejendommen er en doven samling.

Lad os derefter i vores implementering af servicelag eksplicit afgrænse vores transaktionsgrænse ved hjælp af @Transaktionel:

@Service offentlig klasse SimpleUserService implementerer UserService {privat endelig UserRepository userRepository; offentlig SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @ Override @ Transactional (readOnly = true) offentlig Valgfri findOne (streng brugernavn) {returner userRepository.findByUsername (brugernavn); }}

3.1. Forventningen

Her er hvad vi forventer at ske, når vores kode kalder findOne metode:

  1. Først opfanger Spring-proxyen opkaldet og får den aktuelle transaktion eller opretter en, hvis der ikke findes nogen.
  2. Derefter delegerer det metodekaldet til vores implementering.
  3. Endelig forpligter fuldmægtigen transaktionen og lukker følgelig den underliggende Session. Når alt kommer til alt, har vi kun brug for det Session i vores servicelag.

I findOne metodeimplementering initialiserede vi ikke tilladelser kollektion. Derfor bør vi ikke være i stand til at bruge tilladelser efter metoden vender tilbage. Hvis vi gentager denne ejendom, vi skulle få en LazyInitializationException.

3.2. Velkommen til den virkelige verden

Lad os skrive en simpel REST-controller for at se, om vi kan bruge tilladelser ejendom:

@RestController @RequestMapping ("/ brugere") offentlig klasse UserController {privat endelig UserService userService; offentlig UserController (UserService userService) {this.userService = userService; } @GetMapping ("/ {brugernavn}") public ResponseEntity findOne (@PathVariable String username) {return userService .findOne (username) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound () .build ()); }}

Her gentager vi det tilladelser under konvertering af enhed til DTO. Da vi forventer, at konvertering mislykkes med en LazyInitializationException, følgende test skal ikke bestå:

@SpringBootTest @ AutoConfigureMockMvc @ ActiveProfiles ("test") klasse UserControllerIntegrationTest {@Autowired privat UserRepository userRepository; @Autowired privat MockMvc mockMvc; @BeforeEach ugyldig setUp () {Brugerbruger = ny bruger (); user.setUsername ("root"); user.setPermissions (nyt HashSet (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (bruger); } @Test ugyldigt givetTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () kaster undtagelse {mockMvc.perform (get ("/ users / root")). OgExpect (status (). IsOk ()). Og Expect (jsonPath ("$." Brugernavn)). root ")) .andExpect (jsonPath (" $. tilladelser ", indeholderInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

Denne test kaster dog ikke nogen undtagelser, og den består.

Fordi OSIV skaber en Session i begyndelsen af ​​anmodningen, den transaktionsmæssige fuldmagtbruger den tilgængelige strøm Session i stedet for at skabe en helt ny.

Så på trods af hvad vi kunne forvente, kan vi faktisk bruge tilladelser ejendom, selv uden for en eksplicit @Transaktionel. Desuden kan disse slags dovne foreninger hentes hvor som helst i det nuværende anmodningsomfang.

3.3. Om udviklerproduktivitet

Hvis OSIV ikke var aktiveret, skulle vi manuelt initialisere alle nødvendige dovne foreninger i en transaktionel kontekst. Den mest rudimentære (og normalt forkerte) måde er at bruge Hibernate.initialize () metode:

@ Override @ Transactional (readOnly = sand) offentlig Valgfri findOne (streng brugernavn) {Valgfri bruger = userRepository.findByUsername (brugernavn); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); tilbagevendende bruger }

På nuværende tidspunkt er effekten af ​​OSIV på udviklerens produktivitet åbenbar. Det handler dog ikke altid om udviklerens produktivitet.

4. Performance skurk

Antag, at vi er nødt til at udvide vores enkle brugertjeneste til ring til en anden fjerntjeneste efter at have hentet brugeren fra databasen:

@ Override public Valgfri findOne (streng brugernavn) {Valgfri bruger = userRepository.findByUsername (brugernavn); hvis (bruger.isPresent ()) {// fjernopkald} returnerer bruger; }

Her fjerner vi @Transaktionel kommentar, da vi tydeligvis ikke vil beholde forbindelsen Session mens du venter på fjerntjenesten.

4.1. Undgå blandede IO'er

Lad os afklare, hvad der sker, hvis vi ikke fjerner @Transaktionel kommentar. Antag at den nye fjerntjeneste reagerer lidt langsommere end normalt:

  1. Først får Spring-proxyen strømmen Session eller opretter en ny. Uanset hvad, dette Session er ikke forbundet endnu. Det vil sige, det bruger ikke nogen forbindelse fra puljen.
  2. Når vi udfører forespørgslen for at finde en bruger, Session bliver forbundet og låner en Forbindelse fra poolen.
  3. Hvis hele metoden er transaktionel, fortsætter metoden med at ringe til den langsomme fjerntjeneste, mens den lånte holdes Forbindelse.

Forestil dig, at i løbet af denne periode får vi en række opkald til findOne metode. Så efter et stykke tid, alt Forbindelser kan vente på svar fra dette API-opkald. Derfor, Vi kan snart løbe tør for databaseforbindelser.

At blande database-IO'er med andre typer IO'er i en transaktionel sammenhæng er en dårlig lugt, og vi bør undgå det for enhver pris.

Alligevel, siden vi fjernede @Transaktionel kommentar fra vores service, vi forventer at være sikre.

4.2. Udtømning af forbindelsespoolen

Når OSIV er aktivt, der er altid en Session i det nuværende anmodningsomfang, selvom vi fjerner det @Transaktionel. Selvom dette Session er ikke oprindeligt forbundet, efter vores første database IO, bliver den tilsluttet og forbliver sådan indtil slutningen af ​​anmodningen.

Så vores uskyldige udseende og nyligt optimerede serviceimplementering er en opskrift på katastrofe i nærværelse af OSIV:

@ Override public Valgfri findOne (streng brugernavn) {Valgfri bruger = userRepository.findByUsername (brugernavn); hvis (bruger.isPresent ()) {// fjernopkald} returnerer bruger; }

Her er hvad der sker, mens OSIV er aktiveret:

  1. I begyndelsen af ​​anmodningen opretter det tilsvarende filter et nyt Session.
  2. Når vi kalder findByUsername metode, at Session låner en Forbindelse fra poolen.
  3. Det Session forbliver forbundet indtil slutningen af ​​anmodningen.

Selvom vi forventer, at vores servicekode ikke udtømmer forbindelsespuljen, kan den blotte tilstedeværelse af OSIV potentielt gøre hele applikationen ikke reagerer.

For at gøre tingene endnu værre, årsagen til problemet (langsom fjerntjeneste) og symptomet (databaseforbindelsespuljen) er ikke relateret. På grund af denne lille sammenhæng er sådanne ydelsesproblemer vanskelige at diagnosticere i produktionsmiljøer.

4.3. Unødvendige forespørgsler

Desværre er udtømning af forbindelsespuljen ikke det eneste OSIV-relaterede præstationsproblem.

Siden den Session er åben i hele anmodningens livscyklus, nogle ejendomsnavigationer kan udløse et par flere uønskede forespørgsler uden for transaktionskonteksten. Det er endda muligt at ende med n + 1-valgproblemet, og den værste nyhed er, at vi muligvis ikke bemærker dette før produktionen.

Tilføje fornærmelse mod skade, Session udfører alle disse ekstra forespørgsler i auto-commit-tilstand. I auto-commit-tilstand behandles hver SQL-sætning som en transaktion og begås automatisk lige efter at den er udført. Dette lægger igen meget pres på databasen.

5. Vælg klogt

Om OSIV er et mønster eller et antimønster er irrelevant. Det vigtigste her er den virkelighed, vi lever i.

Hvis vi udvikler en simpel CRUD-tjeneste, kan det være fornuftigt at bruge OSIV, da vi muligvis aldrig støder på disse præstationsproblemer.

På den anden side, hvis vi befinder os i at ringe til mange fjerntjenester, eller der sker så meget uden for vores transaktionssammenhæng, anbefales det stærkt at deaktivere OSIV helt.

I tvivlstilfælde skal du starte uden OSIV, da vi let kan aktivere det senere. På den anden side kan deaktivering af et allerede aktiveret OSIV være besværligt, da vi muligvis skal håndtere en masse LazyInitializationExceptions.

Bundlinjen er, at vi skal være opmærksomme på kompromiserne, når vi bruger eller ignorerer OSIV.

6. Alternativer

Hvis vi deaktiverer OSIV, skal vi på en eller anden måde forhindre potentiale LazyInitializationExceptions når man beskæftiger sig med dovne foreninger. Blandt en håndfuld tilgange til håndtering af dovne foreninger vil vi opregne to af dem her.

6.1. Enhedsgrafer

Når vi definerer forespørgselsmetoder i Spring Data JPA, kan vi kommentere en forespørgselsmetode med @EntityGraph at ivrigt hente en del af enheden:

offentlig grænseflade UserRepository udvider JpaRepository {@EntityGraph (attributePaths = "tilladelser") Valgfri findByUsername (String-brugernavn); }

Her definerer vi en ad-hoc-enhedsgraf for at indlæse tilladelser attribut ivrigt, selvom det er en doven samling som standard.

Hvis vi har brug for at returnere flere fremskrivninger fra den samme forespørgsel, skal vi definere flere forespørgsler med forskellige enhedsgrafkonfigurationer:

offentlig grænseflade UserRepository udvider JpaRepository {@EntityGraph (attributePaths = "tilladelser") Valgfri findDetailedByUsername (String-brugernavn); Valgfri findSummaryByUsername (streng brugernavn); }

6.2. Advarsler ved brug Hibernate.initialize ()

Man kan argumentere for, at vi i stedet for at bruge enhedsgrafer kan bruge det berygtede Hibernate.initialize () at hente dovne foreninger, uanset hvor vi har brug for det:

@ Override @ Transactional (readOnly = true) offentlig Valgfri findOne (streng brugernavn) {Valgfri bruger = userRepository.findByUsername (brugernavn); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); tilbagevendende bruger }

De kan være kloge om det og foreslår også at ringe til getPermissions () metode til at udløse hentningsprocessen:

Valgfri bruger = userRepository.findByUsername (brugernavn); user.ifPresent (u -> {Sæt tilladelser = u.getPermissions (); System.out.println ("Tilladelser indlæst:" + tilladelser. størrelse ());});

Begge tilgange anbefales ikke siden de pådrager sig (mindst) en ekstra forespørgselud over den oprindelige at hente den dovne forening. Dvs. genererer dvale følgende forespørgsler for at hente brugere og deres tilladelser:

> vælg u.id, u.username fra brugere u hvor u.username =? > vælg p.user_id, p.permissies fra user_permissions p hvor p.user_id =? 

Selvom de fleste databaser er ret gode til at udføre den anden forespørgsel, bør vi undgå den ekstra netværksrundtur.

På den anden side, hvis vi bruger enhedsgrafer eller endda Hent sammenføjninger, vil Dvaletilstand hente alle de nødvendige data med kun en forespørgsel:

> vælg u.id, u.username, p.user_id, p.tilladelser fra brugere u venstre ydre join user_permissions p på u.id = p.user_id hvor u.username =?

7. Konklusion

I denne artikel vendte vi vores opmærksomhed mod en ret kontroversiel funktion i foråret og et par andre virksomhedsrammer: Open Session in View. For det første blev vi aquatinted med dette mønster både konceptuelt og implementeringsmæssigt. Derefter analyserede vi det ud fra produktivitets- og præstationsperspektiver.

Som sædvanlig er prøvekoden tilgængelig på GitHub.


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