Ú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:
- 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.
- Minden alkalommal, amikor az alkalmazásnak szüksége van a Ülés, újból felhasználja a már létezőt.
- 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:
- 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.
- Ezután a módszerhívást delegálja a megvalósításunkba.
- 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:
- 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.
- 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.
- 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:
- A kérelem elején a megfelelő szűrő létrehoz egy újat Ülés.
- Amikor felhívjuk a findByUsername módszer, az Ülés hitelt vesz fel a Kapcsolat a medencéből.
- 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.