En guide til vårens åpne sesjon i sikte

1. Oversikt

Økt per forespørsel er et transaksjonsmønster for å binde utholdenhetsøkten og be om livssykluser sammen. Ikke overraskende kommer Spring med sin egen implementering av dette mønsteret, kalt OpenSessionInViewInterceptor, for å legge til rette for å jobbe med late foreninger og dermed forbedre utviklerens produktivitet.

I denne veiledningen skal vi først lære hvordan interceptor fungerer internt, og så vil vi se hvordan dette kontroversielle mønsteret kan være et tveegget sverd for våre applikasjoner!

2. Vi introduserer Open Session in View

For å bedre forstå rollen som Open Session in View (OSIV), la oss anta at vi har en innkommende forespørsel:

  1. Våren åpner et nytt dvalemodus Økt i begynnelsen av forespørselen. Disse Økter ikke nødvendigvis er koblet til databasen.
  2. Hver gang applikasjonen trenger en Økt, den vil gjenbruke den allerede eksisterende.
  3. På slutten av forespørselen lukker den samme fangeren det Økt.

Ved første øyekast kan det være fornuftig å aktivere denne funksjonen. Tross alt håndterer rammeverket opprettelse og avslutning av økten, slik at utviklerne ikke bekymrer seg for disse tilsynelatende lave detaljene. Dette øker i sin tur utviklerens produktivitet.

Noen ganger, OSIV kan forårsake subtile ytelsesproblemer i produksjonen. Vanligvis er disse typer problemer veldig vanskelig å diagnostisere.

2.1. Vårstøvel

OSIV er som standard aktivt i Spring Boot-applikasjoner. Til tross for det, fra og med Spring Boot 2.0, advarer det oss om at det er aktivert ved oppstart av applikasjonen hvis vi ikke har konfigurert det eksplisitt:

spring.jpa.open-in-view er aktivert som standard. Derfor kan databasespørringer utføres under visningsrendering. Konfigurer eksplisitt spring.jpa.open-in-view for å deaktivere denne advarselen

Uansett kan vi deaktivere OSIV ved å bruke spring.jpa.open-in-view konfigurasjonsegenskap:

spring.jpa.open-in-view = false

2.2. Mønster eller motmønster?

Det har alltid vært blandede reaksjoner mot OSIV. Hovedargumentet til pro-OSIV-leiren er utviklerens produktivitet, spesielt når det gjelder late assosiasjoner.

På den annen side er problemer med databasens ytelse det viktigste argumentet for anti-OSIV-kampanjen. Senere skal vi vurdere begge argumentene i detalj.

3. Lazy Initialization Hero

Siden OSIV binder Økt livssyklus til hver forespørsel, Dvalemodus kan løse late assosiasjoner selv etter retur fra en eksplisitt @Transaksjonell service.

For å bedre forstå dette, la oss anta at vi modellerer brukerne våre og deres sikkerhetstillatelser:

@Entity @Table (name = "brukere") offentlig klasse bruker {@Id @GeneratedValue private Lang id; privat streng brukernavn; @ElementCollection private Set-tillatelser; // getters og setters}

I likhet med andre en-til-mange og mange-til-mange forhold, tillatelser eiendommen er en lat samling.

La oss i vår implementering av tjenestelag eksplisitt avgrense transaksjonsgrensen ved hjelp av @Transaksjonell:

@Service offentlig klasse SimpleUserService implementerer UserService {private final UserRepository userRepository; offentlig SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @ Override @ Transactional (readOnly = true) offentlig Valgfri findOne (streng brukernavn) {return userRepository.findByUsername (brukernavn); }}

3.1. Forventningen

Her er hva vi forventer å skje når koden vår kaller Finn én metode:

  1. Først avlytter Spring-proxyen samtalen og får den gjeldende transaksjonen eller oppretter en hvis ingen eksisterer.
  2. Deretter delegerer den metodekallet til implementeringen vår.
  3. Til slutt forplikter fullmakten transaksjonen og lukker følgelig den underliggende Økt. Tross alt trenger vi bare det Økt i vårt tjenestelag.

I Finn én metodeimplementering initialiserte vi ikke tillatelser samling. Derfor bør vi ikke kunne bruke tillatelser etter metoden returnerer. Hvis vi gjentar det på denne eiendommen, vi skulle få en LazyInitializationException.

3.2. Velkommen til den ekte verden

La oss skrive en enkel REST-kontroller for å se om vi kan bruke tillatelser eiendom:

@RestController @RequestMapping ("/ brukere") offentlig klasse UserController {private final UserService userService; offentlig UserController (UserService userService) {this.userService = userService; } @GetMapping ("/ {brukernavn}") public ResponseEntity findOne (@PathVariable String username) {return userService .findOne (username) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound ().bygge()); }}

Her gjentar vi det tillatelser under enhet til DTO-konvertering. Siden vi forventer at konvertering mislykkes med en LazInitializationException, følgende test skal ikke bestå:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles ("test") klasse UserControllerIntegrationTest {@Autowired privat UserRepository userRepository; @Autowired privat MockMvc mockMvc; @BeforeEach void setUp () {User user = new User (); user.setUsername ("root"); user.setPermissions (ny HashSet (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (bruker); } @Test ugyldig gittTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () kaster unntak {mockMvc.perform (get ("/ users / root")). Og Expect (status (). IsOk ()). AndExpect (jsonPath ("$." Username "). root ")) .andExpect (jsonPath (" $. permissions ", inneholderInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

Imidlertid kaster ikke denne testen noen unntak, og den består.

Fordi OSIV skaper en Økt i begynnelsen av forespørselen, transaksjonsfullmaktenbruker gjeldende tilgjengelig Økt i stedet for å lage en helt ny.

Så, til tross for hva vi kan forvente, kan vi faktisk bruke tillatelser eiendom selv utenfor en eksplisitt @Transaksjonell. Dessuten kan denne typen late assosiasjoner hentes hvor som helst i gjeldende forespørselsomfang.

3.3. Om utviklerproduktivitet

Hvis OSIV ikke var aktivert, måtte vi initialisere alle nødvendige late assosiasjoner manuelt i en transaksjonell sammenheng. Den mest rudimentære (og vanligvis gale) måten er å bruke Hibernate.initialize () metode:

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

Nå er effekten av OSIV på utviklerens produktivitet åpenbar. Imidlertid handler det ikke alltid om utviklerens produktivitet.

4. Performance Skurk

Anta at vi må utvide vår enkle brukertjeneste til ring en annen ekstern tjeneste etter å ha hentet brukeren fra databasen:

@ Override public Valgfritt findOne (String brukernavn) {Optional user = userRepository.findByUsername (username); hvis (user.isPresent ()) {// ekstern samtale} returnerer bruker; }

Her fjerner vi @Transaksjonell kommentar siden vi tydeligvis ikke vil beholde tilkoblingen Økt mens du venter på fjerntjenesten.

4.1. Unngå blandede IOer

La oss avklare hva som skjer hvis vi ikke fjerner @Transaksjonell kommentar. Anta at den nye fjerntjenesten svarer litt saktere enn vanlig:

  1. Først får vår-fullmektig strømmen Økt eller oppretter en ny. Uansett, dette Økt er ikke tilkoblet ennå. Det vil si at den ikke bruker noen tilkobling fra bassenget.
  2. Når vi utfører spørringen for å finne en bruker, blir Økt blir koblet sammen og låner a Forbindelse fra bassenget.
  3. Hvis hele metoden er transaksjonsmessig, fortsetter metoden til å ringe den langsomme eksterne tjenesten mens den beholder den lånte Forbindelse.

Tenk deg at i løpet av denne perioden får vi en rekke anrop til Finn én metode. Så, etter en stund, alt Tilkoblinger kan vente på svar fra den API-samtalen. Derfor, vi kan snart gå tom for databaseforbindelser.

Å blande database-IO-er med andre typer IO-er i en transaksjonell sammenheng er en dårlig lukt, og vi bør unngå det for enhver pris.

Uansett, siden vi fjernet @Transaksjonell kommentar fra tjenesten vår, vi forventer å være trygge.

4.2. Tømmer tilkoblingsbassenget

Når OSIV er aktivt, det er alltid en Økt i gjeldende forespørselsomfang, selv om vi fjerner @Transaksjonell. Selv om dette Økt er ikke koblet til i utgangspunktet, etter vår første database IO, blir den koblet og forblir slik til slutten av forespørselen.

Så vår uskyldige og nylig optimaliserte tjenesteimplementering er en oppskrift på katastrofe i nærvær av OSIV:

@ Override public Valgfritt findOne (String brukernavn) {Optional user = userRepository.findByUsername (username); hvis (user.isPresent ()) {// ekstern samtale} returnerer bruker; }

Slik skjer mens OSIV er aktivert:

  1. I begynnelsen av forespørselen oppretter det tilsvarende filteret et nytt Økt.
  2. Når vi kaller findByUsername metode, det Økt låner a Forbindelse fra bassenget.
  3. De Økt forblir tilkoblet til slutten av forespørselen.

Selv om vi forventer at tjenestekoden vår ikke vil tømme tilkoblingsbassenget, kan bare OSIVs tilstedeværelse gjøre at hele applikasjonen ikke svarer.

For å gjøre saken enda verre, årsaken til problemet (langsom ekstern tjeneste) og symptomet (databaseforbindelsesbassenget) er ikke relatert. På grunn av denne lille korrelasjonen er det vanskelig å diagnostisere slike ytelsesproblemer i produksjonsmiljøer.

4.3. Unødvendige spørsmål

Dessverre er det ikke det eneste OSIV-relaterte ytelsesproblemet å tømme koblingsbassenget.

Siden Økt er åpen for hele forespørselens livssyklus, noen eiendomsnavigasjoner kan utløse noen flere uønskede spørsmål utenfor transaksjonssammenheng. Det er til og med mulig å ende opp med n + 1 velg problem, og den verste nyheten er at vi kanskje ikke merker dette før produksjonen.

Legge til fornærmelse mot skade, Økt utfører alle de ekstra spørsmålene i auto-commit-modus. I auto-commit-modus blir hver SQL-setning behandlet som en transaksjon og blir automatisk begått rett etter at den er utført. Dette legger igjen stort press på databasen.

5. Velg klokt

Om OSIV er et mønster eller et antimønster er irrelevant. Det viktigste her er virkeligheten vi lever i.

Hvis vi utvikler en enkel CRUD-tjeneste, kan det være fornuftig å bruke OSIV, da vi kanskje aldri møter disse ytelsesproblemene.

På den andre siden, hvis vi finner oss selv i å ringe mange eksterne tjenester, eller det skjer så mye utenfor våre transaksjonelle sammenhenger, anbefales det sterkt å deaktivere OSIV helt.

Når du er i tvil, start uten OSIV, siden vi enkelt kan aktivere det senere. På den annen side kan det være vanskelig å deaktivere et allerede aktivert OSIV, da vi kanskje trenger å håndtere mye av det LazyInitializationExceptions.

Poenget er at vi bør være oppmerksomme på kompromissene når vi bruker eller ignorerer OSIV.

6. Alternativer

Hvis vi deaktiverer OSIV, bør vi på en eller annen måte forhindre potensial LazyInitializationExceptions når du arbeider med late assosiasjoner. Blant en håndfull tilnærminger for å takle late assosiasjoner, skal vi oppregne to av dem her.

6.1. Enhetsgrafer

Når vi definerer spørringsmetoder i Spring Data JPA, kan vi kommentere en spørringsmetode med @EntityGraph å ivrig hente en del av enheten:

offentlig grensesnitt UserRepository utvider JpaRepository {@EntityGraph (attributePaths = "permissions") Valgfritt findByUsername (String brukernavn); }

Her definerer vi en ad-hoc-enhetsgraf for å laste inn tillatelser attributt ivrig, selv om det er en lat samling som standard.

Hvis vi trenger å returnere flere projeksjoner fra samme spørring, bør vi definere flere spørsmål med forskjellige grafkonfigurasjoner:

offentlig grensesnitt UserRepository utvider JpaRepository {@EntityGraph (attributePaths = "permissions") Valgfritt findDetailedByUsername (String brukernavn); Valgfritt findSummaryByUsername (String brukernavn); }

6.2. Advarsler ved bruk Hibernate.initialize ()

Man kan hevde at i stedet for å bruke enhetsdiagrammer, kan vi bruke det beryktede Hibernate.initialize () å hente late assosiasjoner hvor som helst vi trenger å gjøre det:

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

De kan være flinke med det og foreslår også å ringe getPermissions () metode for å utløse hentingsprosessen:

Valgfri bruker = userRepository.findByUsername (brukernavn); user.ifPresent (u -> {Angi tillatelser = u.getPermissions (); System.out.println ("Lastede tillatelser:" + tillatelser. størrelse ());});

Begge tilnærmingene er ikke anbefalt siden de pådrar seg (minst) ett ekstra spørsmål, i tillegg til den opprinnelige, for å hente den late assosiasjonen. Det vil si at dvalemodus genererer følgende spørsmål for å hente brukere og deres tillatelser:

> velg u.id, u.username fra brukere u hvor u.username =? > velg p.user_id, p.permissies fra user_permissions p hvor p.user_id =? 

Selv om de fleste databaser er ganske flinke til å utføre den andre spørringen, bør vi unngå den ekstra nettverksrundturen.

På den annen side, hvis vi bruker enhetsdiagrammer eller til og med Fetch Joins, vil Hibernate hente alle nødvendige data med bare ett spørsmål:

> velg u.id, u.username, p.user_id, p.permissions from users u left outer join user_permissions p på u.id = p.user_id hvor u.username =?

7. Konklusjon

I denne artikkelen vendte vi oppmerksomheten mot en ganske kontroversiell funksjon i løpet av våren og noen få andre forretningsrammer: Open Session in View. Først ble vi vannet med dette mønsteret både konseptuelt og implementeringsmessig. Så analyserte vi det fra produktivitets- og ytelsesperspektiv.

Eksempelkoden er som vanlig tilgjengelig på GitHub.


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