Útmutató a tavasz nyílt munkamenetéhez

1. Áttekintés

A kérelemenkénti munkamenet egy tranzakciós minta, amely összekapcsolja a perzisztencia munkamenetet és a kérés életciklusait. Nem meglepő, hogy a Tavasz a saját, ennek a mintának a megvalósításával érkezik OpenSessionInViewInterceptor, a lusta egyesületekkel való munka megkönnyítése és ezáltal a fejlesztői termelékenység javítása érdekében.

Ebben az oktatóanyagban először megismerjük az elfogó belső működését, majd meglátjuk, hogyan lehet ez az ellentmondásos minta kétélű kard az alkalmazásaink számára!

2. Bemutatjuk a nyílt munkamenetet a nézetben

Tegyük fel, hogy beérkező kérésünk van ahhoz, hogy jobban megértsük az Open Session in View (OSIV) nézetben betöltött szerepét:

  1. A tavasz új hibernált állapotot nyit Ülés a kérés elején. Ezek Munkamenetek nem feltétlenül kapcsolódnak az adatbázishoz.
  2. Minden alkalommal, amikor az alkalmazásnak szüksége van a Ülés, újból felhasználja a már létezőt.
  3. A kérelem végén ugyanaz az elfogó ezt lezárja Ülés.

Első pillantásra értelmes lehet engedélyezni ezt a funkciót. Végül is a keretrendszer kezeli a munkamenet létrehozását és befejezését, így a fejlesztők nem foglalkoznak ezekkel az alacsonynak tűnő részletekkel. Ez pedig növeli a fejlesztők termelékenységét.

Néha azonban Az OSIV finom teljesítményproblémákat okozhat a gyártásban. Általában az ilyen típusú problémákat nagyon nehéz diagnosztizálni.

2.1. Tavaszi csizma

Alapértelmezés szerint az OSIV aktív a Spring Boot alkalmazásokban. Ennek ellenére a Spring Boot 2.0-tól kezdve figyelmeztet minket arra, hogy engedélyezve van az alkalmazás indításakor, ha nem állítottuk be kifejezetten:

A spring.jpa.open-in-view alapértelmezés szerint engedélyezve van. Ezért adatbázis-lekérdezéseket lehet végrehajtani a nézetmegjelenítés során. Ennek a figyelmeztetésnek a letiltásához pontosan konfigurálja a spring.jpa.open-in-view alkalmazást.

Mindenesetre letilthatjuk az OSIV-t a tavasz.jpa.nyitva konfigurációs tulajdonság:

spring.jpa.open-in-view = hamis

2.2. Minta vagy anti-minta?

Az OSIV-re mindig vegyes reakciók voltak. Az OSIV-párti tábor fő érve a fejlesztői termelékenység, különösen a lusta egyesületekkel való foglalkozás során.

Másrészt az adatbázis-teljesítmény kérdései jelentik az OSIV-ellenes kampány elsődleges érvét. Később mindkét érvet részletesen értékelni fogjuk.

3. Lusta inicializáló hős

Mivel az OSIV köti a Ülés életciklus minden kéréshez, A hibernátum képes megoldani a lusta asszociációkat, még akkor is, ha kifejezetten visszatér @ Tranzakció szolgáltatás.

Ennek jobb megértése érdekében tegyük fel, hogy modellezzük felhasználóinkat és biztonsági engedélyeiket:

@Entity @Table (name = "users") public class User {@Id @GeneratedValue private Long id; privát String felhasználónév; @ElementCollection private Set engedélyek; // szerelők és beállítók}

A többi egy-sok és sok-sok kapcsolathoz hasonlóan a engedélyeket ingatlan lusta gyűjtemény.

Ezután a szolgáltatási réteg megvalósításakor határozzuk meg határozottan a tranzakciós határunkat a segítségével @ Tranzakció:

@Service public class SimpleUserService implementálja a UserService {private final UserRepository userRepository; public SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @Orride @Transactional (readOnly = true) public Opcionális findOne (String felhasználónév) {return userRepository.findByUsername (felhasználónév); }}

3.1. A várakozás

Itt várható, hogy mi fog történni, amikor kódunk meghívja a találj egyet módszer:

  1. Eleinte a tavaszi proxy elfogja a hívást, és megkapja az aktuális tranzakciót, vagy létrehoz egy ilyet, ha még nincs ilyen.
  2. Ezután a módszerhívást delegálja a megvalósításunkba.
  3. Végül a proxy elköveti a tranzakciót, és ennek következtében lezárja az alapul szolgáló ügyletet Ülés. Végül is csak erre van szükségünk Ülés szolgáltatási rétegünkben.

Ban,-ben találj egyet metódus megvalósítását, nem inicializáltuk a engedélyeket Gyűjtemény. Ezért nem kellene tudni használni a engedélyeket utána a metódus visszatér. Ha iterálunk ezen a tulajdonságon, meg kellene szereznünk egy LazyInitializationException.

3.2. Isten hozott a való világban

Írjunk egy egyszerű REST vezérlőt, hátha tudjuk használni a engedélyeket ingatlan:

@RestController @RequestMapping ("/ users") public class UserController {private final UserService userService; public UserController (UserService userService) {this.userService = userService; } @GetMapping ("/ {felhasználónév}") public ResponseEntity findOne (@PathVariable String felhasználónév) {return userService .findOne (felhasználónév) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound ().épít()); }}

Itt iterálunk engedélyeket az entitásból a DTO konverzióba. Mivel arra számítunk, hogy az átalakítás kudarcot vall a LazyInitializationException, a következő teszt nem felel meg:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles ("test") osztály UserControllerIntegrationTest {@Autowired private UserRepository userRepository; @Autowired privát MockMvc mockMvc; @BeforeEach void setUp () {Felhasználó felhasználó = új Felhasználó (); user.setUsername ("root"); user.setPermissions (új HashSet (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (felhasználó); } @Test void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () dobja a Kivételt {mockMvc.perform (get ("/ felhasználók / root")) .ésExpect (status (). IsOk ()) .andExpect (jsonPath ("$ (felhasználónév)". root ")) .andExpect (jsonPath (" $. permissions ", tartalmazzaInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

Ez a teszt azonban nem vet fel kivételeket, és átmegy.

Mivel az OSIV létrehoz egy Ülés a kérés elején az ügyleti meghatalmazotta rendelkezésre álló áramot használja Ülés ahelyett, hogy vadonatúj alkotna.

Tehát annak ellenére, amire számíthatunk, valóban használhatjuk a engedélyeket akár kifejezetten kívül is @ Tranzakció. Ezenkívül az ilyen típusú lusta társulások bárhonnan megszerezhetők a jelenlegi kérési körben.

3.3. A fejlesztői termelékenységről

Ha az OSIV nincs engedélyezve, akkor az összes szükséges lusta társítást kézzel kell inicializálnunk egy tranzakciós kontextusban. A legalapvetőbb (és általában helytelen) módszer a Hibernálás.inicialize () módszer:

@Orride @Transactional (readOnly = true) public Opcionális findOne (String felhasználónév) {Opcionális user = userRepository.findByUsername (felhasználónév); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); visszatérő felhasználó; }

Mostanra az OSIV hatása a fejlesztői termelékenységre nyilvánvaló. Ez azonban nem mindig a fejlesztői termelékenységről szól.

4. Teljesítménygonosz

Tegyük fel, hogy ki kell terjesztenünk egyszerű felhasználói szolgáltatásunkat hívjon egy másik távoli szolgáltatást, miután lekérte a felhasználót az adatbázisból:

@Override public Opcionális findOne (String felhasználónév) {Opcionális user = userRepository.findByUsername (felhasználónév); if (user.isPresent ()) {// távhívás} visszatérő felhasználó; }

Itt eltávolítjuk a @ Tranzakció kommentár, mivel nyilvánvalóan nem akarjuk fenntartani a kapcsolatot Ülés miközben a távoli szolgáltatásra várt.

4.1. Kerülje a vegyes IO-kat

Tisztázzuk, mi történik, ha nem távolítjuk el a @ Tranzakció annotáció. Tegyük fel, hogy az új távoli szolgáltatás a szokásosnál kissé lassabban reagál:

  1. Eleinte a tavaszi proxy kapja meg az áramot Ülés vagy újat hoz létre. Akárhogy is, ez Ülés még nem csatlakozik. Vagyis nem használ semmilyen kapcsolatot a készletből.
  2. Miután végrehajtottuk a lekérdezést, hogy megtaláljuk a felhasználót, a Ülés összekapcsolódik és kölcsönveszi a Kapcsolat a medencéből.
  3. Ha az egész módszer tranzakciós, akkor a módszer hívja a lassú távoli szolgáltatást, miközben a kölcsönzöttet megtartja Kapcsolat.

Képzelje el, hogy ebben az időszakban egy sor hívást kapunk a találj egyet módszer. Aztán egy idő után minden Kapcsolatok várhat az adott API-hívás válaszára. Ebből kifolyólag, hamarosan elfogyhatnak az adatbázis-kapcsolatok.

Az adatbázis-IO-k keverése más típusú IO-kkal tranzakciós összefüggésben rossz szag, és ezt mindenáron el kell kerülnünk.

Egyébként is, mivel eltávolítottuk a @ Tranzakció a szolgáltatásunkból érkezett kommentár, biztonságra számítunk.

4.2. A Connection Pool kimerítése

Amikor az OSIV aktív, mindig van egy Ülés az aktuális kérelem hatókörében, akkor is, ha eltávolítjuk @ Tranzakció. Bár ez Ülés az első adatbázis-IO után nem csatlakozik eredetileg, és csatlakozik, és a kérés végéig így is marad.

Tehát az ártatlan külsejű és a közelmúltban optimalizált szolgáltatásunk a katasztrófa receptje az OSIV jelenlétében:

@Override public Opcionális findOne (String felhasználónév) {Opcionális user = userRepository.findByUsername (felhasználónév); if (user.isPresent ()) {// távhívás} visszatérő felhasználó; }

A következők történnek, miközben az OSIV engedélyezve van:

  1. A kérelem elején a megfelelő szűrő létrehoz egy újat Ülés.
  2. Amikor felhívjuk a findByUsername módszer, az Ülés hitelt vesz fel a Kapcsolat a medencéből.
  3. A Ülés a kérelem végéig kapcsolatban marad.

Annak ellenére, hogy arra számítunk, hogy a szolgáltatási kódunk nem meríti ki a kapcsolati készletet, az OSIV puszta jelenléte potenciálisan válaszképtelenné teheti az egész alkalmazást.

Hogy még rosszabb legyen a helyzet, a probléma kiváltó oka (lassú távoli szolgáltatás) és a tünet (adatbázis-kapcsolatkészlet) nincsenek kapcsolatban. E kevés összefüggés miatt az ilyen teljesítményproblémákat nehéz diagnosztizálni a termelési környezetekben.

4.3. Felesleges lekérdezések

Sajnos a kapcsolati készlet kimerítése nem az egyetlen OSIV-lel kapcsolatos teljesítményprobléma.

Mivel a Ülés a kérelem teljes életciklusára nyitva van, egyes tulajdonságok közötti navigáció még néhány nem kívánt lekérdezést válthat ki a tranzakciós kontextuson kívül. Még az is lehetséges, hogy n + 1 kiválasztási problémával járunk, és a legrosszabb hír az, hogy ezt csak a gyártásig tudjuk észrevenni.

A sérülést sértő hozzáadással a Ülés az összes további lekérdezést automatikus elkötelezettség módban hajtja végre. Automatikus elkötelezettség módban az egyes SQL utasításokat tranzakcióként kezelik, és a végrehajtás után azonnal végrehajtják. Ez viszont nagy nyomást gyakorol az adatbázisra.

5. Válassza okosan

Az, hogy az OSIV minta vagy anti-minta, lényegtelen. A legfontosabb itt a valóság, amelyben élünk.

Ha egy egyszerű CRUD szolgáltatást fejlesztünk, akkor érdemes lehet OSIV-t használni, mivel soha nem találkozhatunk ezekkel a teljesítményproblémákkal.

Másrészről, Ha sok távoli szolgáltatást hívunk magunkhoz, vagy annyi minden történik a tranzakciós kontextusunkon kívül, akkor nagyon ajánlott az OSIV teljes letiltása.

Ha kétségei vannak, indítsa el az OSIV nélkül, mivel később könnyedén engedélyezhetjük. Másrészt a már engedélyezett OSIV letiltása nehézkes lehet, mivel sokakat kell kezelnünk Lusta InicializálásKivételek.

A lényeg az, hogy tisztában kell lennünk a kompromisszumokkal, amikor az OSIV-t használjuk vagy figyelmen kívül hagyjuk.

6. Alternatívák

Ha letiltjuk az OSIV-t, akkor valahogy meg kell akadályoznunk a potenciált Lusta InicializálásKivételek lusta társulásokkal foglalkozva. A lusta egyesületekkel való megbirkózás néhány megközelítése közül kettőt fogunk itt felsorolni.

6.1. Entitásgrafikonok

Amikor a Spring Data JPA-ban meghatározzuk a lekérdezési módszereket, akkor a lekérdezési módszert feljegyezhetjük @EntityGraph lelkesen előhívni az entitás valamely részét:

nyilvános felület A UserRepository kiterjeszti a JpaRepository {@EntityGraph (attributePaths = "engedélyek") Opcionális findByUsername (String felhasználónév); }

Itt meghatározunk egy ad-hoc entitásdiagramot a engedélyeket lelkesen tulajdonítson, pedig alapból lusta gyűjtemény.

Ha ugyanarról a lekérdezésről több vetületet is vissza kell adnunk, akkor több lekérdezést is meg kell határoznunk, különböző entitásdiagram-konfigurációkkal:

nyilvános felület A UserRepository kiterjeszti a JpaRepository {@EntityGraph (attributePaths = "engedélyek") Opcionális findDetailedByUsername (karakterlánc felhasználónév); Opcionális findSummaryByUsername (karakterlánc felhasználónév); }

6.2. Figyelmeztetések a használat során Hibernálás.inicialize ()

Azt állíthatjuk, hogy az entitásdiagramok helyett a notóriust is használhatjuk Hibernálás.inicialize () lusta egyesületeket szerezni bárhová, ahol erre szükségünk van:

@Orride @Transactional (readOnly = true) public Opcionális findOne (String felhasználónév) {Opcionális user = userRepository.findByUsername (felhasználónév); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); visszatérő felhasználó; }

Lehet, hogy ügyesek hozzá, és azt is javasolják, hogy hívják a getPermissions () módszer a lekérési folyamat elindítására:

Választható user = userRepository.findByUsername (felhasználónév); user.ifPresent (u -> {Engedélyek beállítása = u.getPermissions (); System.out.println ("Betöltött engedélyek:" + permissions.size ());});

Mindkét megközelítés azóta sem ajánlott (legalább) egy további lekérdezést hajtanak végre, az eredeti mellett a lusta egyesület beolvasására. Vagyis a Hibernate a következő lekérdezéseket generálja a felhasználók és engedélyeik lekérésére:

> válassza az u.id, u.username felhasználótól u ahol u.username =? > válassza a p.user_id, p.engedélyeket a user_permissions oldalról p ahol p.user_id =? 

Bár a legtöbb adatbázis elég jól képes végrehajtani a második lekérdezést, el kell kerülnünk az extra hálózati oda-vissza utat.

Másrészről, ha entitásdiagramokat vagy akár Fetch Joins-t használunk, a Hibernate minden szükséges adatot egyetlen lekérdezéssel kapna:

> válassza az u.id, u.username, p.user_id, p.engedélyeket a felhasználóktól u bal külső csatlakozzon user_permissions p az u.id = p.user_id helyre, ahol u.username =?

7. Következtetés

Ebben a cikkben egy meglehetősen ellentmondásos szolgáltatásra irányítottuk figyelmünket tavasszal és néhány más vállalati keretrendszerre: Open Session in View. Először fogalmaztuk meg ezt a mintát fogalmi és megvalósítási szempontból egyaránt. Ezután a termelékenység és a teljesítmény szempontjából elemeztük.

Szokás szerint a mintakód elérhető a GitHubon.


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