3 uobičajena problema s performansama hibernacije i kako ih pronaći u datoteci dnevnika

1. Uvod

Vjerojatno ste pročitali neke pritužbe na loše performanse hibernacije ili ste se možda i sami borili s nekima od njih. Hibernate koristim već više od 15 godina i naišao sam na više nego dovoljno ovih problema.

Tijekom godina naučio sam da se ti problemi mogu izbjeći i da ih možete pronaći puno u svojoj datoteci dnevnika. U ovom postu želim vam pokazati kako možete pronaći i popraviti 3 od njih.

2. Pronađite i riješite probleme s izvedbom

2.1. Zabilježite SQL izjave u proizvodnji

Prvo izdanje izvedbe izuzetno je lako uočiti i često se ignorira. To je bilježenje SQL izraza u proizvodnom okruženju.

Pisanje nekih izjava iz dnevnika ne zvuči veliko, a postoji puno aplikacija koje rade upravo to. Ali izuzetno je neučinkovit, posebno putem System.out.println kao što to čini Hibernate ako postavite show_sql parametar u vašoj hibernaciji u pravi:

Hibernacija: odaberite order0_.id kao id1_2_, order0_.orderNumber kao orderNum2_2_, order0_.version kao version3_2_ od purchaseOrder order0_ Hibernacija: odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_0_, items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id =? Hibernacija: odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_0_, items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity kao quantity2_0_1_, items0_.version kao version3_0_1_ od OrderItem items0_ gdje items0_. id_naredbe =? Hibernacija: odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_0_, items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity kao quantity2_0_1_, items0_.version kao version3_0_1_ od OrderItem items0_ gdje items0_. id_naredbe =?

U jednom od svojih projekata popravio sam performanse za 20% u roku od nekoliko minuta postavljanjem show_sql do lažno. To je vrsta postignuća o kojoj želite izvijestiti na sljedećem stand-up sastanku 🙂

Prilično je očito kako možete riješiti ovaj problem s izvedbom. Samo otvorite svoju konfiguraciju (npr. Svoju datoteku persistence.xml) i postavite show_sql parametar do lažno. Ove vam informacije ionako nisu potrebne u proizvodnji.

Ali možda će vam trebati tijekom razvoja. Ako to ne učinite, koristite dvije različite konfiguracije hibernacije (što ne biste smjeli). I tamo ste deaktivirali evidentiranje SQL izraza. Rješenje za to je korištenje dvije različite konfiguracije dnevnika za razvoj i proizvodnju koje su optimizirane za specifične zahtjeve okoline izvođenja.

Konfiguracija razvoja

Konfiguracija razvoja trebala bi pružiti što više korisnih informacija kako biste mogli vidjeti kako Hibernate komunicira s bazom podataka. Stoga biste trebali barem prijaviti generirane SQL izjave u vašu razvojnu konfiguraciju. To možete učiniti aktiviranjem DEBUG poruka za org.hibernate.SQL kategorija. Ako također želite vidjeti vrijednosti svojih parametara vezanja, morate postaviti razinu dnevnika org.hibernate.type.descriptor.sql do TRAG:

log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPonversionPC d {HH: mm: ss, SSS}% -5p [% c] -% m% n log4j.rootLogger = info, stdout # osnovna razina dnevnika za sve poruke log4j.logger.org.hibernate = info # SQL izrazi i parametri log4j.logger.org.hibernate.SQL = otklanjanje pogrešaka log4j.logger.org.hibernate.type.descriptor.sql = trag

Sljedeći isječak koda prikazuje neke primjere poruka dnevnika koje Hibernate zapisuje s ovom konfiguracijom dnevnika. Kao što vidite, dobivate detaljne informacije o izvršenom SQL upitu i svim postavljenim i preuzetim vrijednostima parametara:

23: 03: 22.246 DEBUG SQL: 92 - odaberite order0_.id kao id1_2_, order0_.orderNumber kao orderNum2_2_, order0_.version kao version3_2_ from purchaseOrder order0_ where order0_.id = 1 23: 03: 22.254 TRACE BasicExtractor: 61 - ekstrahirana vrijednost ( [id1_2_]: [BIGINT]) - [1] 23: 03: 22,261 TRACE BasicExtractor: 61 - izvučena vrijednost ([orderNum2_2_]: [VARCHAR]) - [order1] 23: 03: 22,263 TRACE BasicExtractor: 61 - izvučena vrijednost ( [verzija3_2_]: [INTEGER]) - [0]

Hibernate vam pruža puno više internih informacija o a Sjednica ako aktivirate statistiku hibernacije. To možete učiniti postavljanjem svojstva sustava hibernate.generate_statistics istinitom.

Ali, molim vas, aktivirajte samo statistiku vašeg razvojnog ili testnog okruženja. Prikupljanje svih ovih podataka usporava vašu aplikaciju i sami biste mogli stvoriti probleme s izvedbom ako je aktivirate u proizvodnji.

Neke primjere statistike možete vidjeti u sljedećem isječku koda:

23: 04: 12.123 INFO StatisticsLoggingSessionEventListener: 258 - Metrika sesije {23793 nanosekunde potrošene u stjecanju 1 JDBC veza; 0 nanosekundi potrošeno na oslobađanje 0 JDBC veza; 394686 nanosekundi potrošenih na pripremu 4 JDBC izjave; 2528603 nanosekunde potrošene na izvršavanje 4 JDBC izraza; 0 nanosekundi provedeno u izvršavanju 0 JDBC serija; 0 nanosekundi provedeno u izvođenju 0 L2C stavljanja; 0 nanosekundi provedeno u izvođenju 0 L2C pogodaka; 0 nanosekundi provedeno izvodeći 0 promašaja L2C; Potrošeno 9700599 nanosekundi u izvršavanju 1 ispiranja (ispiranje ukupno 9 entiteta i 3 zbirke); 42921 nanosekundi provedeno u izvršavanju 1 djelomičnog ispiranja (ispiranje ukupno 0 entiteta i 0 zbirki)}

Te statistike redovito koristim u svakodnevnom radu kako bih pronašao probleme s izvedbom prije nego što se pojave u proizvodnji i mogao bih napisati nekoliko postova upravo o tome. Dakle, usredotočimo se samo na najvažnije.

Redci 2 do 5 pokazuju vam koliko je JDBC veza i izjava Hibernate upotrijebio tijekom ove sesije i koliko je vremena potrošio na nju. Uvijek biste trebali pogledati ove vrijednosti i usporediti ih sa svojim očekivanjima.

Ako postoji puno više izjava nego što ste očekivali, najvjerojatnije imate najčešći problem s izvedbom, problem s odabirom n + 1. Možete ga pronaći u gotovo svim aplikacijama, a mogao bi stvoriti velike probleme s performansama na većoj bazi podataka. Objasnit ću ovo pitanje detaljnije u sljedećem odjeljku.

Redci 7 do 9 pokazuju kako je Hibernate komunicirao s predmemorijom druge razine. Ovo je jedna od 3 predmemorije Hibernate-a i pohranjuje entitete na neovisan način o sesiji. Ako u svojoj aplikaciji koristite drugu razinu, uvijek biste trebali nadgledati ove statistike da biste vidjeli hoće li Hibernate odatle dobiti entitete.

Konfiguracija proizvodnje

Proizvodna konfiguracija treba biti optimizirana za izvedbu i izbjegavati poruke koje nisu hitno potrebne. To općenito znači da biste trebali prijavljivati ​​samo poruke o pogreškama. Ako koristite Log4j, to možete postići sljedećom konfiguracijom:

Ako koristite Log4j, to možete postići sljedećom konfiguracijom:

log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPlay% ConversionP. d {HH: mm: ss, SSS}% -5p [% c] -% m% n log4j.rootLogger = info, stdout # osnovna razina dnevnika za sve poruke log4j.logger.org.hibernate = pogreška

2.2. N + 1 Odaberite izdanje

Kao što sam već objasnio, problem s odabirom n + 1 najčešći je problem izvedbe. Mnogi programeri za ovaj problem krive koncept OR-Mapping i nisu u potpunosti u krivu. Ali to možete lako izbjeći ako razumijete kako se Hibernate odnosi prema lijeno dohvaćenim vezama. Stoga je kriv i programer jer je njegova odgovornost izbjegavati takve probleme. Dakle, najprije ću vam objasniti zašto postoji ovaj problem, a zatim vam pokazati jednostavan način da ga spriječite. Ako ste već upoznati s problemima odabira n + 1, možete prijeći izravno na rješenje.

Hibernate pruža vrlo povoljno mapiranje odnosa između entiteta. Trebate samo atribut s vrstom povezanog entiteta i nekoliko bilješki da biste ga definirali:

@Entity @Table (name = "purchaseOrder") narudžba javne klase implementira serializable {@OneToMany (mappedBy = "order", fetch = FetchType.LAZY) private Set items = new HashSet (); ...}

Kada sada učitate Narudžba iz baze podataka, samo trebate nazvati getItems () metoda za dobivanje svih predmeta ove narudžbe. Hibernate skriva potrebne upite baze podataka da bi dobio povezane Artikl narudžbe entiteti iz baze podataka.

Kada ste započeli s hibernacijom, vjerojatno ste naučili da biste trebali koristiti FetchType.LIJENO za većinu veza i da je zadana za mnoge veze. To Hibernaciji govori da dohvaća povezane entitete samo ako koristite atribut koji preslikava odnos. Dohvat samo podataka koji su vam potrebni općenito je dobra stvar, ali također zahtijeva Hibernate da izvrši dodatni upit za inicijalizaciju svake veze. To može rezultirati velikim brojem upita, ako radite na popisu entiteta, kao što radim u sljedećem isječku koda:

Popis naloga = em.createQuery ("ODABERI O IZ Naloga o"). GetResultList (); za (Narudžba narudžbe: narudžbe) {log.info ("Narudžba:" + narudžba.getOrderNumber ()); log.info ("Broj stavki:" + order.getItems (). size ()); }

Vjerojatno ne biste očekivali da ovih nekoliko redaka koda može stvoriti stotine ili čak tisuće upita baze podataka. Ali ako koristite FetchType.LIJENO za odnos prema Artikl narudžbe entitet:

22: 47: 30,065 DEBUG SQL: 92 - odaberite order0_.id kao id1_2_, order0_.orderNumber kao orderNum2_2_, order0_.version kao version3_2_ from purchaseOrder order0_ 22: 47: 30,136 INFO NamedEntityGraphTest: 58 - Order: order1 22: 47: 30,140 DEBUG SQL: 92 - odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_0_, items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity kao items_0_.00_0. items0_.order_id =? 22: 47: 30,171 INFO NamedEntityGraphTest: 59 - Broj predmeta: 2 22: 47: 30,171 INFO NamedEntityGraphTest: 58 - Narudžba: order2 22: 47: 30,172 DEBUG SQL: 92 - odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_. , items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id =? 22: 47: 30,174 INFO NamedEntityGraphTest: 59 - Broj predmeta: 2 22: 47: 30,174 INFO NamedEntityGraphTest: 58 - Narudžba: nalog3 22: 47: 30,174 DEBUG SQL: 92 - odaberite items0_.order_id kao order_id4_0_0_, items0_.id kao id1_0_. , items0_.id kao id1_0_1_, items0_.order_id kao order_id4_0_1_, items0_.product_id kao product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id =? 22: 47: 30,176 INFO NamedEntityGraphTest: 59 - Broj predmeta: 2

Hibernate izvodi jedan upit da bi dobio sve Narudžba entiteta i dodatni za svaki od n Narudžba entiteti za inicijalizaciju stavka narudžbe odnos. Dakle, sada znate zašto se takva vrsta problema naziva n + 1 select problem i zašto može stvoriti velike probleme s izvedbom.

Još je gore to što ga često ne prepoznajete u maloj testnoj bazi podataka ako niste provjerili statistiku hibernacije. Isječak koda zahtijeva samo nekoliko desetaka upita ako testna baza podataka ne sadrži puno narudžbi. Ali to će biti potpuno drugačije ako koristite produktivnu bazu podataka koja ih sadrži nekoliko tisuća.

Rekao sam ranije da te probleme možete lako izbjeći. I to je istina. Jednostavno morate inicijalizirati odnos orderItem kada odaberete Narudžba entiteti iz baze podataka.

Ali, molim vas, učinite to samo ako odnos koristite u poslovnom kodu, a ne koristite FetchType.EAGER uvijek dohvatiti povezane entitete. To samo zamjenjuje vaš problem s n + 1 drugim problemom u izvedbi.

Inicijalizirajte odnose s @NamedEntityGraph

Postoji nekoliko različitih opcija za inicijalizaciju odnosa. Više volim koristiti @NamedEntityGraph što je jedna od mojih najdražih značajki predstavljenih u JPA 2.1. Pruža neovisan način upita za određivanje grafa entiteta koje će Hibernate preuzeti iz baze podataka. U sljedećem isječku koda možete vidjeti primjer jednostavnog grafa koji omogućava hibernaciji da željno dohvaća atribut items entiteta:

@Entity @Table (name = "order_order") @NamedEntityGraph (name = "graph.Order.items", attributeNodes = @NamedAttributeNode ("items")) javna klasa Redoslijed implementira Serializable {...}

Ne morate puno učiniti da definirate graf entiteta s @NamedEntityGraph bilješka. Morate navesti jedinstveni naziv za grafikon i jedan @NamedAttributeNode napomena za svaki atribut Hibernate će se željno dohvatiti. U ovom primjeru samo atribut items preslikava odnos između Narudžba i nekoliko Artikl narudžbe entiteta.

Sada grafikon entiteta možete koristiti za upravljanje ponašanjem dohvaćanja ili određenim upitom. Stoga morate instancirati EntityGraph bazirano na @NamedEntityGraph definiciju i dati je kao nagovještaj EntityManager.find () metodu ili vaš upit. To radim u sljedećem isječku koda gdje odabirem Narudžba entitet s id 1 iz baze podataka:

Grafikon EntityGraph = this.em.getEntityGraph ("graph.Order.items"); Savjeti karte = novi HashMap (); hints.put ("javax.persistence.fetchgraph", grafikon); vratite this.em.find (Order.class, 1L, hints);

Hibernate koristi ove podatke za stvaranje jednog SQL izraza koji dobiva atribute Narudžba entitet i atributi grafikona entiteta iz baze podataka:

17: 34: 51.310 DEBUG [org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter] (pool-2-thread-1) LoadPlan (entity = blog.oughts.on.java.jpa21.entity.graph.model. Narudžba) - Povratak - EntityReturnImpl (entity = blog.oughts.on.java.jpa21.entity.graph.model.Order, querySpaceUid =, path = blog.oughts.on.java.jpa21.entity.graph.model.Order) - CollectionAttributeFetchImpl (collection = blog.oughts.on.java.jpa21.entity.graph.model.Order.items, querySpaceUid =, path = blog.oughts.on.java.jpa21.entity.graph.model.Order.items) - (element kolekcije) CollectionFetchableElementEntityGraph (entity = blog.oughts.on.java.jpa21.entity.graph.model.OrderItem, querySpaceUid =, path = blog.oughts.on.java.jpa21.entity.graph.model.Order. stavke.) - EntityAttributeFetchImpl (entity = blog.oughts.on.java.jpa21.entity.graph.model.Product, querySpaceUid =, path = blog.oughts.on.java.jpa21.entity.graph.model.Order.items ..product) - QuerySpaces - EntityQuerySpaceImpl (uid =, entity = blog.oughts.on.java.jpa21.entity.graph.model .Naručivanje) - Mapiranje zamjenskog imena tablice SQL - redoslijed0_ - sufiks zamjenskog imena - 0_ - sufiksirani stupci ključeva - {id1_2_0_} - PRIDRUŽITE (JoinDefinedByMetadata (stavke)): -> - CollectionQuerySpaceImpl (uid =, collection = blog.oughts.on.java. jpa21.entity.graph.model.Order.items) - mapiranje aliasa SQL tablice - items1_ - sufiks zamjenskog imena - 1_ - sufiksirani stupci ključeva - {order_id4_2_1_} - sufiks zamjenskog zamjenskog imena entiteta - 2_ - 2_entity-element sufiksirani stupci ključnih riječi - id1_0_2_ - PRIDRUŽITE (JoinDefinedByMetadata (elementi)): -> - EntityQuerySpaceImpl (uid =, entity = blog.oughts.on.java.jpa21.entity.graph.model.OrderItem) - Mapiranje zamjenskih imena SQL tablica - items1_ - sufiks aliasa - 2_ - sufiks ključni stupci - {id1_0_2_} - PRIDRUŽITE (JoinDefinedByMetadata (proizvod)): -> - EntityQuerySpaceImpl (uid =, entity = blog.oughts.on.java.jpa21.entity.graph.model.Product) - mapiranje aliasa SQL tablice - product2_ - alias sufiks - 3_ - sufiksirani stupci ključeva - {id1_1_3_} 17: 34: 51,311 DEBUG [org.hibernate.loader.entity.plan.EntityLoader] (pool-2-thread-1) Statički odabir f ili entitet blog.oughts.on.java.jpa21.entity.graph.model.Order [NONE: -1]: odaberite order0_.id kao id1_2_0_, order0_.orderNumber kao orderNum2_2_0_, order0_.version kao version3_2_0_, items1_.order_id kao order_id4_2_1_ . .verzija kao verzija3_1_3_ iz naloga za narudžbu_narudžbe0_ lijevo vanjsko pridruživanje stavke OrderItem1_ on order0_.id = items1_.order_id lijevo vanjsko spajanje Product product2_ on items1_.product_id = product2_.id gdje order0_.id =?

Inicijalizacija samo jednog odnosa dovoljno je dobra za objavu na blogu, ali u stvarnom projektu najvjerojatnije ćete htjeti napraviti složenije grafikone. Pa učinimo to.

Možete, naravno, pružiti niz @NamedAttributeNode napomene za dohvaćanje više atributa istog entiteta i možete ih koristiti @NamedSubGraph za definiranje ponašanja dohvaćanja za dodatnu razinu entiteta. Koristim to u sljedećem isječku koda za dohvaćanje ne samo svih povezanih Artikl narudžbe entiteti, ali i Proizvod entitet za svaku Artikal narudžbe:

@Entity @Table (name = "order_order") @NamedEntityGraph (name = "graph.Order.items", attributeNodes = @NamedAttributeNode (value = "items", subgraph = "items"), subgraphs = @NamedSubgraph (name = " items ", attributeNodes = @NamedAttributeNode (" product "))) javna klasa Order implementira Serializable {...}

Kao što vidite, definicija a @NamedSubGraph je vrlo slična definiciji a @NamedEntityGraph. Zatim se na ovaj podgraf možete pozvati u a @NamedAttributeNode napomena za definiranje ponašanja dohvaćanja za ovaj specifični atribut.

Kombinacija ovih bilješki omogućuje vam definiranje složenih grafikona entiteta pomoću kojih možete inicijalizirati sve odnose koje koristite u vašem slučaju upotrebe i izbjeći n + 1 probleme s odabirom. Ako želite dinamički navesti graf entiteta tijekom izvođenja, to možete učiniti i putem Java API-ja.

2.3. Ažurirajte entitete jedan po jedan

Ažuriranje entiteta jedan po jedan osjeća se vrlo prirodno ako razmišljate na objektno orijentiran način. Samo dobijete entitete koje želite ažurirati i pozovete nekoliko metoda postavljača za promjenu njihovih atributa kao što to radite s bilo kojim drugim objektom.

Ovaj pristup dobro funkcionira ako promijenite samo nekoliko entiteta.Ali to postaje vrlo neučinkovito kada radite s popisom entiteta i to je treće pitanje performansi koje možete lako uočiti u svojoj datoteci dnevnika. Jednostavno morate potražiti hrpu SQL UPDATE izjava koje izgledaju potpuno isto, kao što možete vidjeti u sljedećoj datoteci dnevnika:

22: 58: 05,829 DEBUG SQL: 92 - odaberite product0_.id kao id1_1_, product0_.name kao name2_1_, product0_.cena kao cijena3_1_, product0_.verzija kao verzija4_1_ iz Product product0_ 22: 58: 05,883 DEBUG SQL: 92 - ažuriranje Set proizvoda ime = ?, cijena = ?, verzija =? gdje id =? i verzija =? 22: 58: 05,889 DEBUG SQL: 92 - ažuriranje Naziv skupa proizvoda = ?, cijena = ?, verzija =? gdje id =? i verzija =? 22: 58: 05,891 DEBUG SQL: 92 - ažuriranje Naziv skupa proizvoda = ?, cijena = ?, verzija =? gdje id =? i verzija =? 22: 58: 05,893 DEBUG SQL: 92 - ažuriranje Naziv skupa proizvoda = ?, cijena = ?, verzija =? gdje id =? i verzija =? 22: 58: 05,900 DEBUG SQL: 92 - ažuriranje Naziv skupa proizvoda = ?, cijena = ?, verzija =? gdje id =? i verzija =?

Relacijski prikaz zapisa baze podataka puno je bolji za ove slučajeve uporabe od objektno orijentiranog. Pomoću SQL-a možete napisati samo jedan SQL izraz koji ažurira sve zapise koje želite promijeniti.

To možete učiniti s Hibernate ako koristite JPQL, izvorni SQL ili CriteriaUpdate API. Sve tri vrlo slične, pa upotrijebimo JPQL u ovom primjeru.

Izraz JPQL UPDATE možete definirati na sličan način kao što ga znate iz SQL-a. Vi samo definirate koji entitet želite ažurirati, kako promijeniti vrijednosti njegovih atributa i ograničiti zahvaćene entitete u izrazu WHERE.

Primjer toga možete vidjeti u sljedećem isječku koda gdje povećavam cijenu svih proizvoda za 10%:

em.createQuery ("AŽURIRANJE proizvoda p SET p.price = p.price * 0,1"). executeUpdate ();

Hibernate kreira SQL UPDATE izraz temeljen na JPQL izrazu i šalje ga u bazu podataka koja izvodi operaciju ažuriranja.

Prilično je očito da je ovaj pristup puno brži ako morate ažurirati velik broj entiteta. Ali ima i nedostatak. Hibernate ne zna na koje entitete utječe operacija ažuriranja i ne ažurira svoju predmemoriju 1. razine. Stoga biste trebali paziti da ne čitate i ne ažurirate entitet s JPQL izrazom u istoj hibernacijskoj sesiji ili ga morate odvojiti da biste ga uklonili iz predmemorije.

3. Sažetak

Unutar ovog posta pokazao sam vam 3 Hibernate problemi s performansama koje možete pronaći u datotekama dnevnika.

2 od njih uzrokovani su ogromnim brojem SQL izraza. To je čest razlog za probleme s izvedbom ako radite s hibernacijom. Hibernate skriva pristup bazi podataka iza svog API-ja, što često otežava pogađanje stvarnog broja SQL izraza. Stoga biste uvijek trebali provjeriti izvršene SQL izraze kada mijenjate razinu svoje postojanosti.