Sprievodca jarným otvoreným zasadnutím

1. Prehľad

Relácia na žiadosť je transakčný vzorec, ktorý spája reláciu perzistencie a životné cykly. Nie je prekvapením, že Spring prichádza s vlastnou implementáciou tohto modelu s názvom OpenSessionInViewInterceptor, na uľahčenie práce s lenivými asociáciami, a teda na zvýšenie produktivity vývojárov.

V tomto tutoriáli sa najskôr dozvieme, ako interceptor funguje interne, a potom uvidíme, ako tento kontroverzný vzor môže byť pre naše aplikácie dvojsečným mečom!

2. Predstavujeme Open Session v zobrazení

Aby sme lepšie pochopili úlohu otvorenej relácie v zobrazení (OSIV), predpokladajme, že máme prichádzajúcu požiadavku:

  1. Jar otvára nový režim dlhodobého spánku Session na začiatku žiadosti. Títo Relácie nie sú nevyhnutne spojené s databázou.
  2. Zakaždým, keď aplikácia potrebuje a Zasadnutie, znova použije už existujúcu.
  3. Na konci žiadosti to ten istý zachytávač uzavrie Session.

Na prvý pohľad by mohlo mať zmysel povoliť túto funkciu. Rámec koniec koncov rieši vytváranie a ukončovanie relácií, takže vývojári sa týmito zdanlivo nízkoúrovňovými detailmi nezaoberajú. To zase zvyšuje produktivitu vývojárov.

Niekedy však OSIV môže spôsobiť jemné problémy s výkonom vo výrobe. Zvyčajne je ťažké diagnostikovať tieto typy problémov.

2.1. Jarná topánka

Predvolene je OSIV aktívny v aplikáciách Spring Boot. Napriek tomu nás od Spring Boot 2.0 varuje pred skutočnosťou, že je povolená pri štarte aplikácie, ak sme ju výslovne nenakonfigurovali:

spring.jpa.open-in-view je predvolene povolená. Preto sa počas vykresľovania zobrazenia môžu vykonávať databázové dotazy. Explicitne nakonfigurujte spring.jpa.open-in-view na vypnutie tohto varovania

Každopádne môžeme OSIV deaktivovať pomocou spring.jpa.open-in-view vlastnosť konfigurácie:

spring.jpa.open-in-view = false

2.2. Vzor alebo Anti-Pattern?

Na OSIV sa vždy vyskytli zmiešané reakcie. Hlavným argumentom tábora pro-OSIV je produktivita vývojárov, najmä pri práci s lenivými asociáciami.

Na druhej strane problémy s výkonom databázy sú primárnym argumentom anti-OSIV kampane. Neskôr budeme podrobne posudzovať obidva argumenty.

3. Lenivý inicializačný hrdina

Pretože OSIV viaže Session životný cyklus každej žiadosti, Hibernácia dokáže vyriešiť lenivé asociácie aj po návrate z explicitného @ Transakčné služby.

Aby sme tomu lepšie porozumeli, predpokladajme, že modelujeme našich používateľov a ich bezpečnostné povolenia:

@Entity @Table (name = "users") verejná trieda Používateľ {@Id @GeneratedValue private Long id; súkromné ​​reťazcové používateľské meno; @ElementCollection private Nastaviť povolenia; // zakladatelia a zakladatelia}

Podobne ako iné vzťahy typu one-to-many a many-to-many, aj povolenia majetok je lenivá zbierka.

Potom v našej implementácii servisnej vrstvy explicitne ohraničme našu transakčnú hranicu pomocou @ Transakčné:

@ Verejná trieda @Service SimpleUserService implementuje UserService {private final UserRepository userRepository; public SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @Override @Transactional (readOnly = true) public Voliteľné findOne (reťazec používateľské meno) {návrat userRepository.findByUsername (používateľské meno); }}

3.1. Očakávanie

Očakávame, že sa stane, keď náš kód zavolá findOne metóda:

  1. Spočiatku Spring proxy zachytí hovor a získa aktuálnu transakciu alebo ju vytvorí, ak žiadna neexistuje.
  2. Potom deleguje volanie metódy na našu implementáciu.
  3. Napokon proxy splní transakciu a následne uzavrie podkladovú transakciu Session. Koniec koncov, potrebujeme iba to Session v našej servisnej vrstve.

V findOne implementácia metódy, neinicializovali sme povolenia zbierka. Preto by sme nemali byť schopní používať povolenia po metóda sa vráti. Ak túto vlastnosť iterujeme, mali by sme dostať LazyInitializationException.

3.2. Vitajte v skutočnom svete

Poďme napísať jednoduchý radič REST, aby sme zistili, či môžeme použiť povolenia nehnuteľnosť:

@RestController @RequestMapping ("/ users") verejná trieda 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 ()); }}

Tu to opakujeme povolenia počas prevodu subjektu na DTO. Pretože očakávame, že konverzia zlyhá s a LazyInitializationException, nasledujúci test by nemal vyhovieť:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles ("test") trieda 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 (užívateľ); } @Test void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () vyvolá výnimku {mockMvc.perform (get ("/ users / root")) .andExpect (status (). IsOk ()). AndExpect (jsonPath ("$. Username"). Value (" root ")) .andExpect (jsonPath (" $. permissions ", containsInAnyOrder (" PERM_READ "," PERM_WRITE ")))); }}

Tento test však nevyvoláva žiadne výnimky a vyhovuje.

Pretože OSIV vytvára a Session na začiatku žiadosti transakčný proxypoužíva aktuálny dostupný Session namiesto vytvorenia úplne nového.

Takže aj napriek tomu, čo by sme mohli čakať, skutočne môžeme použiť povolenia majetku aj mimo výslovného @ Transakčné. Okrem toho je možné tieto druhy lenivých asociácií načítať kdekoľvek v rozsahu aktuálnej požiadavky.

3.3. O produktivite vývojárov

Ak by OSIV nebol povolený, museli by sme ručne inicializovať všetky potrebné lenivé asociácie v transakčnom kontexte. Najzákladnejším (a zvyčajne nesprávnym) spôsobom je použitie súboru Hibernate.initialize () metóda:

@Override @Transactional (readOnly = true) public Voliteľné findOne (reťazec používateľského mena) {Voliteľný používateľ = userRepository.findByUsername (používateľské meno); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); návratový užívateľ; }

V súčasnosti je vplyv OSIV na produktivitu vývojárov zrejmý. Nie vždy to však je o produktivite vývojárov.

4. Predstavenie Zloduch

Predpokladajme, že musíme rozšíriť našu jednoduchú službu pre používateľov na po načítaní používateľa z databázy zavolať inú vzdialenú službu:

@Override public Voliteľné findOne (reťazcové používateľské meno) {Voliteľné užívateľ = userRepository.findByUsername (užívateľské meno); if (user.isPresent ()) {// remote call} návratový užívateľ; }

Tu odstraňujeme @ Transakčné anotáciu, pretože zjavne nebudeme chcieť udržiavať pripojenie Session počas čakania na vzdialenú službu.

4.1. Vyhýbanie sa zmiešaným IO

Poďme si ujasniť, čo sa stane, ak neodstránime @ Transakčné anotácia. Predpokladajme, že nová vzdialená služba reaguje o niečo pomalšie ako zvyčajne:

  1. Spočiatku získa jarný proxy server Session alebo vytvorí nový. Či tak alebo onak, toto Session ešte nie je pripojený. To znamená, že nepoužíva žiadne pripojenie z fondu.
  2. Akonáhle vykonáme dopyt, aby sme našli používateľa, Session spojí sa a požičia si a Pripojenie z bazéna.
  3. Ak je celá metóda transakčná, potom metóda pokračuje volaním pomalej vzdialenej služby, pričom si ponecháva vypožičané Pripojenie.

Predstavte si, že počas tohto obdobia dostaneme dávku hovorov na server findOne metóda. Potom, po chvíli, všetko Pripojenia môže čakať na odpoveď z tohto volania API. Preto čoskoro by sa nám mohli vyčerpať databázové pripojenia.

Miešanie databázových IO s inými typmi IO v transakčnom kontexte je nepríjemným zápachom a mali by sme sa mu za každú cenu vyhnúť.

Každopádne odkedy sme odstránili @ Transakčné anotácie z našej služby, očakávame, že budeme v bezpečí.

4.2. Vyčerpanie skupiny pripojení

Keď je aktívny OSIV, vždy existuje Session v rozsahu aktuálnej žiadosti, aj keď odstránime @ Transakčné. Aj keď toto Session nie je pripojený na začiatku, po našom prvom vstupe a výstupe databázy sa pripojí a zostane ním až do konca žiadosti.

Naša nevinne vyzerajúca a nedávno optimalizovaná implementácia služieb je teda receptom na katastrofu v prítomnosti OSIV:

@Override public Voliteľné findOne (reťazcové používateľské meno) {Voliteľné užívateľ = userRepository.findByUsername (užívateľské meno); if (user.isPresent ()) {// remote call} návratový užívateľ; }

Keď je povolený OSIV, stane sa toto:

  1. Na začiatku žiadosti vytvorí zodpovedajúci filter nový Session.
  2. Keď hovoríme findByUsername metóda, že Session požičiava a Pripojenie z bazéna.
  3. The Session zostáva pripojený až do konca žiadosti.

Aj keď očakávame, že náš kód služby nevyčerpá fond pripojení, samotná prítomnosť OSIV môže potenciálne spôsobiť, že celá aplikácia nebude reagovať.

Aby toho nebolo málo, hlavná príčina problému (pomalá vzdialená služba) a príznak (fond pripojení k databáze) spolu nesúvisia. Z dôvodu tejto malej korelácie je ťažké diagnostikovať také problémy s výkonom v produkčných prostrediach.

4.3. Zbytočné dotazy

Vyčerpanie fondu pripojení bohužiaľ nie je jediným problémom výkonu spojeným s OSIV.

Keďže Session je otvorený počas celého životného cyklu žiadosti, niektoré navigácie v oblasti nehnuteľností môžu spustiť niekoľko ďalších nechcených dotazov mimo transakčného kontextu. Je dokonca možné skončiť s problémom s výberom n + 1 a najhoršou správou je, že si to môžeme všimnúť až pri výrobe.

Pridanie urážky k zraneniu, Session vykoná všetky tieto ďalšie dotazy v režime automatického potvrdenia. V režime automatického potvrdenia sa každý príkaz SQL považuje za transakciu a automaticky sa potvrdí hneď po jeho vykonaní. To zase vytvára veľký tlak na databázu.

5. Vyberte si rozumne

Či je OSIV vzor alebo anti vzor, ​​je irelevantné. Najdôležitejšou vecou je realita, v ktorej žijeme.

Ak vyvíjame jednoduchú službu CRUD, mohlo by mať zmysel používať OSIV, pretože s týmito problémami s výkonom sa možno nikdy nestretneme.

Na druhej strane, ak zistíme, že voláme veľa vzdialených služieb alebo sa toho deje veľa mimo našich transakčných kontextov, dôrazne sa odporúča OSIV úplne vypnúť.

Ak máte pochybnosti, začnite bez OSIV, pretože to môžeme neskôr ľahko povoliť. Na druhej strane, deaktivácia už povoleného OSIV môže byť ťažkopádna, pretože s tým budeme možno musieť veľa pracovať LazyInitializationExceptions.

Záverom je, že by sme si mali byť vedomí kompromisov pri používaní alebo ignorovaní OSIV.

6. Alternatívy

Ak deaktivujeme OSIV, mali by sme nejako zabrániť potenciálu LazyInitializationExceptions pri jednaní s lenivými asociáciami. Spomedzi niekoľkých prístupov k zvládaniu lenivých asociácií tu vymenujeme dva z nich.

6.1. Grafy entít

Pri definovaní dotazovacích metód v Spring Data JPA môžeme anotovať dotazovaciu metódu pomocou @EntityGraph dychtivo načítať niektorú časť entity:

verejné rozhranie UserRepository rozširuje JpaRepository {@EntityGraph (attributePaths = "permissions") Voliteľné findByUsername (reťazec používateľského mena); }

Tu definujeme ad-hoc entitný graf na načítanie povolenia horlivo pripisovať, aj keď je to predvolene lenivá zbierka.

Ak potrebujeme vrátiť viac projekcií z toho istého dotazu, mali by sme definovať viac dotazov s rôznymi konfiguráciami grafov entít:

verejné rozhranie UserRepository rozširuje JpaRepository {@EntityGraph (attributePaths = "permissions") Voliteľné findDetailedByUsername (meno používateľa v reťazci); Voliteľné findSummaryByUsername (meno používateľa reťazca); }

6.2. Upozornenia pri používaní Hibernate.initialize ()

Niekto by mohol namietať, že namiesto použitia grafov entít môžeme použiť notoricky známe Hibernate.initialize () načítať lenivé asociácie všade, kde to potrebujeme:

@Override @Transactional (readOnly = true) public Voliteľné findOne (reťazec používateľského mena) {Voliteľný používateľ = userRepository.findByUsername (používateľské meno); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); návratový užívateľ; }

Môžu byť pre to chytrí a tiež navrhujú zavolať getPermissions () spôsob spustenia procesu načítania:

Voliteľný užívateľ = userRepository.findByUsername (meno používateľa); user.ifPresent (u -> {Set permissions = u.getPermissions (); System.out.println ("Načítané oprávnenia:" + permissions.size ());});

Oba prístupy sa odvtedy neodporúčajú dostávajú (minimálne) jeden dotaz navyše, okrem pôvodnej, na načítanie lenivej asociácie. To znamená, že režim dlhodobého spánku generuje nasledujúce dotazy na načítanie používateľov a ich povolení:

> vyberte u.id, u.username od používateľov u kde u.username =? > vyberte p.user_id, p.permissions z user_permissions p kde p.user_id =? 

Aj keď je väčšina databáz pri vykonávaní druhého dotazu celkom dobrá, mali by sme sa vyhnúť tejto dodatočnej spätnej väzbe v sieti.

Na druhej strane, ak použijeme grafy entít alebo dokonca Fetch Joins, Hibernate načíta všetky potrebné údaje iba pomocou jedného dotazu:

> vyberte u.id, u.username, p.user_id, p.permissions od používateľov u ľavé vonkajšie pripojenie user_permissions p na u.id = p.user_id kde u.username =?

7. Záver

V tomto článku sme zamerali našu pozornosť na dosť kontroverznú vlastnosť jari a niekoľkých ďalších podnikových rámcov: Open Session in View. Najprv sme sa s týmto vzorom spojili koncepčne aj implementačne. Potom sme to analyzovali z hľadiska produktivity a výkonu.

Ako obvykle je vzorový kód k dispozícii na GitHub.


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