Vodič za otvoreno proljetno zasjedanje

1. Pregled

Sesija po zahtjevu je transakcijski obrazac koji povezuje sesiju trajanja i zahtjeva životne cikluse. Nije iznenađujuće što proljeće dolazi s vlastitom implementacijom ovog uzorka, nazvanog OpenSessionInViewInterceptor, kako bi se olakšao rad s lijenim udrugama, a time i poboljšala produktivnost programera.

U ovom uputstvu prvo ćemo naučiti kako interceptor interno djeluje, a zatim ćemo vidjeti kako ovaj kontroverzni obrazac može biti mač s dvije oštrice za naše primjene!

2. Predstavljanje otvorene sesije u prikazu

Da bismo bolje razumjeli ulogu Open Session in View (OSIV), pretpostavimo da imamo dolazni zahtjev:

  1. Proljeće otvara novi zimski san Sjednica na početku zahtjeva. Ovi Sjednice nisu nužno povezani s bazom podataka.
  2. Svaki put kada aplikacija treba Sjednica, ponovno će upotrijebiti već postojeću.
  3. Na kraju zahtjeva, isti presretač to zatvara Sjednica.

Na prvi pogled možda bi imalo smisla omogućiti ovu značajku. Napokon, okvir upravlja kreiranjem i prekidom sesije, tako da se programeri ne bave tim naizgled detaljima niske razine. To, pak, povećava produktivnost programera.

Međutim, ponekad, OSIV može uzrokovati suptilne probleme s izvedbom u proizvodnji. Obično je ove vrste problema vrlo teško dijagnosticirati.

2.1. Proljetni čizme

Prema zadanim postavkama OSIV je aktivan u aplikacijama Spring Boot. Unatoč tome, od Spring Boota 2.0, upozorava nas na činjenicu da je omogućen prilikom pokretanja aplikacije ako ga nismo eksplicitno konfigurirali:

spring.jpa.open-in-view omogućen je prema zadanim postavkama. Stoga se upiti baze podataka mogu izvoditi tijekom prikazivanja prikaza. Izričito konfigurirajte spring.jpa.open-in-view da onemogući ovo upozorenje

U svakom slučaju, OSIV možemo onemogućiti pomoću proljeće.jpa.otvoreno-u-pogledu svojstvo konfiguracije:

proljeće.jpa.open-in-view = false

2.2. Uzorak ili anti-uzorak?

Prema OSIV-u su uvijek bile mješovite reakcije. Glavni argument pro-OSIV kampa je produktivnost programera, posebno kada se radi o lijenim udrugama.

S druge strane, problemi s performansama baze podataka primarni su argument anti-OSIV kampanje. Kasnije ćemo detaljno procijeniti oba argumenta.

3. Junak lijene inicijalizacije

Budući da OSIV veže Sjednica životni ciklus za svaki zahtjev, Hibernate može razriješiti lijene udruge čak i nakon povratka s eksplicitnog @Transational servis.

Da bismo to bolje razumjeli, pretpostavimo da modeliramo svoje korisnike i njihova sigurnosna dopuštenja:

@Entity @Table (name = "users") javni razred Korisnik {@Id @GeneratedValue private Long id; privatno korisničko ime niza; @ElementCollection private Set dopuštenja; // geteri i postavljači}

Slično drugim odnosima jedan-prema-mnogima i mnogo-prema-mnogo, i dozvole imanje je lijena kolekcija.

Zatim, u našoj implementaciji sloja usluga, izričito razgraničimo našu transakcijsku granicu pomoću @Transational:

@Service javna klasa SimpleUserService implementira UserService {private final UserRepository userRepository; javni SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @Override @Transactional (readOnly = true) public Neobavezno findOne (korisničko ime niza) {return userRepository.findByUsername (korisničko ime); }}

3.1. Očekivanje

Evo što očekujemo da se dogodi kada naš kod pozove findOne metoda:

  1. Isprva proxy Spring presreće poziv i dobiva trenutnu transakciju ili je stvara ako nijedna ne postoji.
  2. Zatim delegira poziv metode našoj implementaciji.
  3. Konačno, proxy obavlja transakciju i posljedično zatvara temeljni Sjednica. Napokon, samo nam to treba Sjednica u našem sloju usluge.

U findOne implementaciju metode, nismo inicijalizirali dozvole kolekcija. Stoga ne bismo trebali biti u mogućnosti koristiti dozvole nakon metoda se vraća. Ako ponovimo ovo svojstvo, trebali bismo dobiti a LazyInitializationException.

3.2. Dobrodošli u Stvarni svijet

Napišimo jednostavni REST kontroler da vidimo možemo li koristiti dozvole svojstvo:

@RestController @RequestMapping ("/ users") javna klasa UserController {privatni konačni UserService userService; javni UserController (UserService userService) {this.userService = userService; } @GetMapping ("/ {korisničko ime}") javni ResponseEntity findOne (@PathVariable String korisničko ime) {return userService .findOne (korisničko ime) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound ().izgraditi()); }}

Evo, ponavljamo se dozvole tijekom pretvorbe entiteta u DTO. Budući da očekujemo da će pretvorba uspjeti s a LazyInitializationException, sljedeći test ne bi trebao proći:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles ("test") klasa UserControllerIntegrationTest {@Autowired private UserRepository userRepository; @Autowired private MockMvc mockMvc; @BeforeEach void setUp () {Korisnik = novi korisnik (); user.setUsername ("root"); user.setPermissions (novi HashSet (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (korisnik); } @Test void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () baca izuzetak {mockMvc.perform (get ("/ users / root") .andExpect (status (). IsOk ()) .andExpect (jsonPath ("$. Username"). korijen ")) .andExpect (jsonPath (" $. dozvole ", sadržiInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

Međutim, ovaj test ne donosi iznimke i prolazi.

Budući da OSIV stvara a Sjednica na početku zahtjeva, transakcijski proxykoristi trenutno dostupnu Sjednica umjesto stvaranja potpuno novog.

Dakle, unatoč onome što bismo mogli očekivati, zapravo možemo koristiti dozvole svojstvo čak i izvan eksplicitnog @Transational. Štoviše, ove vrste lijenih udruga mogu se dohvatiti bilo gdje u trenutnom opsegu zahtjeva.

3.3. O produktivnosti programera

Da OSIV nije omogućen, morali bismo ručno inicijalizirati sve potrebne lijene asocijacije u transakcijskom kontekstu. Najčešći (i obično pogrešan) način je korištenje Hibernate.inicialize () metoda:

@Override @Transactional (readOnly = true) public Neobavezno findOne (korisničko ime niza) {Neobavezno user = userRepository.findByUsername (korisničko ime); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); povratni korisnik; }

Do sada je učinak OSIV-a na produktivnost programera očit. Međutim, nije uvijek riječ o produktivnosti programera.

4. Izvođački negativac

Pretpostavimo da svoju jednostavnu korisničku uslugu moramo proširiti na nazovite drugu udaljenu uslugu nakon dohvaćanja korisnika iz baze podataka:

@Override public Neobavezno findOne (korisničko ime niza) {Neobavezno user = userRepository.findByUsername (korisničko ime); if (user.isPresent ()) {// daljinski poziv} return user; }

Ovdje uklanjamo @Transational napomena jer očito ne želimo zadržati vezu Sjednica dok čekate daljinsku uslugu.

4.1. Izbjegavanje miješanih IO-a

Razjasnimo što se događa ako ne uklonimo @Transational bilješka. Pretpostavimo da nova daljinska usluga reagira malo sporije nego inače:

  1. Isprva proxy proxy dobiva struju Sjednica ili stvara novi. U svakom slučaju, ovo Sjednica još nije povezan. Odnosno, ne koristi nikakvu vezu iz bazena.
  2. Jednom kada izvršimo upit za pronalaženje korisnika, Sjednica postaje povezan i posuđuje a Veza s bazena.
  3. Ako je cijela metoda transakcijska, metoda nastavlja pozivati ​​sporu daljinsku uslugu zadržavajući posuđenu Veza.

Zamislite da u tom razdoblju dobivamo niz poziva na findOne metoda. Zatim, nakon nekog vremena, svi Veze može pričekati odgovor s tog API poziva. Stoga, uskoro možemo ostati bez veza s bazom podataka.

Miješanje IO-a baze podataka s drugim vrstama IO-a u transakcijskom kontekstu loš je miris i trebali bismo ga izbjegavati po svaku cijenu.

U svakom slučaju, otkako smo uklonili @Transational napomena naše službe, očekujemo da ćemo biti sigurni.

4.2. Iscrpljujući bazen veze

Kada je OSIV aktivan, uvijek postoji Sjednica u trenutnom opsegu zahtjeva, čak i ako uklonimo @Transational. Iako ovo Sjednica nije spojen u početku, nakon našeg prvog IO baze podataka, povezuje se i ostaje do kraja zahtjeva.

Dakle, naša implementacija usluge nevinog izgleda i nedavno optimizirana recept je za katastrofu u prisutnosti OSIV-a:

@Override public Neobavezno findOne (korisničko ime niza) {Neobavezno user = userRepository.findByUsername (korisničko ime); if (user.isPresent ()) {// daljinski poziv} return user; }

Evo što se događa dok je OSIV omogućen:

  1. Na početku zahtjeva, odgovarajući filtar stvara novi Sjednica.
  2. Kad nazovemo findByUsername metoda, ta Sjednica posuđuje a Veza s bazena.
  3. The Sjednica ostaje povezan do kraja zahtjeva.

Iako očekujemo da naš servisni kod neće iscrpiti spremište veza, puka prisutnost OSIV-a može potencijalno učiniti da cijela aplikacija ne reagira.

Da stvar bude još gora, osnovni uzrok problema (spora usluga na daljinu) i simptom (spremište baze podataka) nisu povezani. Zbog ove male korelacije takve probleme s izvedbom teško je dijagnosticirati u proizvodnim okruženjima.

4.3. Nepotrebni upiti

Nažalost, iscrpljivanje spremišta veza nije jedini problem izvedbe povezan s OSIV-om.

Budući da je Sjednica je otvoren za cijeli životni ciklus zahtjeva, neke navigacije svojstvima mogu pokrenuti još nekoliko neželjenih upita izvan transakcijskog konteksta. Čak je moguće završiti s n + 1 odabranim problemom, a najgora vijest je da to možda nećemo primijetiti do produkcije.

Dodajući uvredu ozljedi, Sjednica izvršava sve one dodatne upite u načinu automatskog urezivanja. U načinu automatskog urezivanja svaki se SQL izraz tretira kao transakcija i automatski se urezuje odmah nakon izvršenja. To, pak, stvara velik pritisak na bazu podataka.

5. Odaberite Mudro

Je li OSIV obrazac ili anti-obrazac, nevažno je. Ovdje je najvažnija stvarnost u kojoj živimo.

Ako razvijamo jednostavnu CRUD uslugu, možda bi imalo smisla koristiti OSIV, jer se možda nikada nećemo susresti s tim problemima s izvedbom.

S druge strane, ako se zateknemo da zovemo puno udaljenih usluga ili se toliko toga događa izvan našeg transakcijskog konteksta, toplo se preporučuje da potpuno onemogućite OSIV.

Ako sumnjate, započnite bez OSIV-a jer ga kasnije možemo lako omogućiti. S druge strane, onemogućavanje već omogućenog OSIV-a može biti nezgrapno, jer ćemo možda morati puno toga riješiti LazyInitializationExceptions.

Dno crta je da bismo trebali biti svjesni kompromisa kada koristimo ili ignoriramo OSIV.

6. Alternative

Ako onemogućimo OSIV, trebali bismo nekako spriječiti potencijal LazyInitializationExceptions kada se bave lijenim udrugama. Među pregršt pristupa suočavanju s lijenim udrugama, ovdje ćemo nabrojati dva.

6.1. Grafikoni entiteta

Kada definiramo metode upita u Spring Data JPA, možemo metodu upita označiti s @EntityGraph željno dohvatiti neki dio entiteta:

javno sučelje UserRepository proširuje JpaRepository {@EntityGraph (attributePaths = "permissions") Izborno findByUsername (korisničko ime niza); }

Ovdje definiramo ad-hoc grafikon entiteta za učitavanje dozvole atribut željno, iako je to zadano lijena kolekcija.

Ako trebamo vratiti više projekcija iz istog upita, tada bismo trebali definirati više upita s različitim konfiguracijama grafa entiteta:

javno sučelje UserRepository proširuje JpaRepository {@EntityGraph (attributePaths = "permissions") Izborno findDetailedByUsername (korisničko ime niza); Neobvezno findSummaryByUsername (korisničko ime niza); }

6.2. Upozorenja prilikom upotrebe Hibernate.inicialize ()

Moglo bi se tvrditi da umjesto korištenja grafikona entiteta možemo koristiti notorni Hibernate.inicialize () dohvatiti lijene asocijacije gdje god to trebamo učiniti:

@Override @Transactional (readOnly = true) public Neobavezno findOne (korisničko ime niza) {Neobavezno user = userRepository.findByUsername (korisničko ime); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); povratni korisnik; }

Možda su pametni u tome i predlažu da nazovu getPermissions () metoda za pokretanje procesa dohvaćanja:

Neobvezno user = userRepository.findByUsername (korisničko ime); user.ifPresent (u -> {Postavi dozvole = u.getPermissions (); System.out.println ("Dozvole učitane:" + permissions.size ());});

Od tada se ne preporučuju oba pristupa imaju (barem) jedan dodatni upit, uz izvornu, dohvatiti lijenu asocijaciju. Odnosno, Hibernate generira sljedeće upite za dohvaćanje korisnika i njihovih dozvola:

> odaberite u.id, u.username od korisnika u gdje je u.username =? > odaberite p.user_id, p.dozvole iz user_permissions p gdje je p.user_id =? 

Iako je većina baza podataka prilično dobra u izvršavanju drugog upita, trebali bismo izbjeći taj dodatni mrežni povratni put.

S druge strane, ako koristimo grafikone entiteta ili čak Fetch Joins, Hibernate će dohvatiti sve potrebne podatke samo s jednim upitom:

> odaberite u.id, u.username, p.user_id, p.dozvole od korisnika u lijevo vanjsko pridruživanje user_permissions p na u.id = p.user_id gdje je u.username =?

7. Zaključak

U ovom smo članku usmjerili pozornost na prilično kontroverznu značajku u proljeće i nekoliko drugih poslovnih okvira: otvorenu sesiju u prikazu. Prvo smo se konceptualno i implementacijski upoznali s ovim uzorkom. Zatim smo ga analizirali iz perspektive produktivnosti i izvedbe.

Kao i obično, uzorak koda dostupan je na GitHubu.


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