Java >> Java tutorial >  >> Tag >> hibernate

Sådan rettes Hibernates advarsel "firstResult/maxResults specificeret med samlingshentning"

En af de mest almindelige anbefalinger til at forbedre ydeevnen af ​​dit persistenslag er at bruge JOIN FETCH klausuler eller EntityGraph s for at hente nødvendige tilknytninger, når en enhed indlæses. Jeg er helt enig i disse anbefalinger, og vi diskuterer dette meget detaljeret i Hibernate Performance Tuning-kurset i Persistence Hub. Men hvis du følger dette råd og ringer til setFirstResult og setMaxResult metoder til at begrænse størrelsen af ​​resultatsættet, vil du se følgende advarsel i din logfil:

HHH000104:firstResult/maxResults specificeret med samlingshentning; anvender i hukommelsen!

Hibernate 5 viser denne advarsel, hvis du kalder setFirstResult eller setMaxResults metoder på en forespørgsel, der bruger en JOIN FETCH klausul eller en EntityGraph . Hibernate 6 forbedrede håndteringen af ​​EntityGraph s og viser kun advarslen, hvis din forespørgsel indeholder en JOIN FETCH klausul.

Hvorfor Hibernate viser HHH000104-advarslen

Årsagen til denne advarsel bliver indlysende, når du kigger på SQL-sætningen, som Hibernate skal generere, når du bruger en JOIN FETCH klausul eller en EntityGraph . Begge tilgange fortæller Hibernate at initialisere en administreret tilknytning mellem 2 enhedsklasser. For at gøre det skal Hibernate slutte sig til de tilknyttede tabeller og vælge alle kolonner, der er kortlagt af enhedsklasserne. Dette kombinerer posterne i begge tabeller og øger størrelsen af ​​resultatsættet. Det giver problemer, hvis du vil begrænse dens størrelse ved at kalde setFirstResult og setMaxResults metoder.

Lad os tage et kig på et eksempel.

Jeg modellerede en mange-til-mange-forbindelse mellem ChessTournament og Skakspilleren enhedsklasser. Den bedste praksis at arbejde med denne tilknytning er at bruge standarden FetchType.LAZY og en JOIN FETCH klausul eller EntityGraph for at initialisere den, hvis det er nødvendigt.

Hibernate henter derefter alle de nødvendige oplysninger ved hjælp af 1 SQL-sætning. Men det udløser den tidligere viste advarsel, hvis du begrænser størrelsen af ​​dit forespørgselsresultat. Du kan se et eksempel på det i følgende kodestykke.

TypedQuery<ChessTournament> q = em.createQuery("""
                                                  SELECT t 
                                                  FROM ChessTournament t 
                                                      LEFT JOIN FETCH t.players
                                                  WHERE t.name LIKE :name""", 
                                               ChessTournament.class);
q.setParameter("name", "%Chess%");
q.setFirstResult(0);
q.setMaxResults(5);
List<ChessTournament> tournaments = q.getResultList();

Som forventet skrev Hibernate advarslen HHH000104 til logfilen. Og den tilføjede ikke en LIMIT- eller OFFSET-klausul for at begrænse resultatsættets størrelse, selvom jeg indstillede firstResult  til 0 og maxResult  til 5.

15:56:57,623 WARN  [org.hibernate.hql.internal.ast.QueryTranslatorImpl] - HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
15:56:57,626 DEBUG [org.hibernate.SQL] - 
    select
        chesstourn0_.id as id1_1_0_,
        chessplaye2_.id as id1_0_1_,
        chesstourn0_.endDate as enddate2_1_0_,
        chesstourn0_.name as name3_1_0_,
        chesstourn0_.startDate as startdat4_1_0_,
        chesstourn0_.version as version5_1_0_,
        chessplaye2_.birthDate as birthdat2_0_1_,
        chessplaye2_.firstName as firstnam3_0_1_,
        chessplaye2_.lastName as lastname4_0_1_,
        chessplaye2_.version as version5_0_1_,
        players1_.ChessTournament_id as chesstou1_2_0__,
        players1_.players_id as players_2_2_0__ 
    from
        ChessTournament chesstourn0_ 
    left outer join
        ChessTournament_ChessPlayer players1_ 
            on chesstourn0_.id=players1_.ChessTournament_id 
    left outer join
        ChessPlayer chessplaye2_ 
            on players1_.players_id=chessplaye2_.id 
    where
        chesstourn0_.name like ?

Årsagen til det bliver synlig, når du udfører den samme sætning i en SQL-klient. Ved at tilmelde dig den administrerede forening og vælge alle kolonner, der er kortlagt af ChessTournament og Skakspiller enhedsklasser, er forespørgslens resultatsæt et produkt af posterne i ChessTournament tabellen og de tilknyttede poster i ChessPlayer tabel.

Hver rekord i resultatsættet er en unik kombination af en turnering og en af ​​dens spillere. Det er den forventede måde, hvordan relationelle databaser håndterer sådan en forespørgsel. Men det skaber et problem, i det specielle tilfælde med en JOIN FETCH klausul eller en EntityGraph .

Normalt bruger Hibernate firstResult og maxResult værdier for at anvende pagineringen i SQL-sætningen. Disse fortæller databasen kun at returnere en del af resultatsættet. I de foregående eksempler kaldte jeg setFirstResult metode med 0 og setMaxResults metode med 5 . Hvis Hibernate ville anvende standardhåndteringen af ​​disse parametre på den genererede SQL-sætning, ville databasen kun returnere de første 5 rækker af resultatsættet. Som du kan se på det følgende billede, indeholder disse optegnelser Tata Steel Chess Tournament 2021 med 4 af dens spillere og Tata Steel Chess Tournament 2022 med 1 af dens spillere.

Men det var ikke det, vi havde til hensigt med JPQL-forespørgslen. Det angivne firstResult og maxResult værdier skulle returnere de første 5 ChessTournament enheder med alle tilknyttede ChessPlayer enheder. De skulle definere paginering for den returnerede ChessTournament enhedsobjekter og ikke af produktet i SQL-resultatsættet.

Det er derfor, Hibernate skriver advarslen til logfilen og anvender pagineringen i hukommelsen. Den udfører SQL-sætningen uden nogen paginering. Databasen returnerer derefter alle ChessTournament enheder og deres tilknyttede ChessPlayer s. Og Hibernate begrænser størrelsen af ​​den returnerede List når den analyserer resultatsættet.

Selvom denne tilgang giver det korrekte resultat, risikerer den dig for alvorlige præstationsproblemer. Afhængigt af størrelsen på din database kan forespørgslen vælge flere tusinde poster og gøre din ansøgning langsommere.

Sådan undgår du HHH000104-advarslen

Den bedste måde at undgå Hibernates advarsel og potentielle ydeevneproblemer er at udføre 2 forespørgsler. Den 1. forespørgsel vælger de primære nøgler for alle ChessTournament enheder, du ønsker at hente. Denne forespørgsel henter ikke tilknytningerne, og du kan bruge setFirstResult og setMaxResult metoder til at begrænse størrelsen af ​​resultatsættet. Den anden henter disse entiteter og deres tilknyttede ChessPlayer s.

TypedQuery<Long> idQuery = em.createQuery("""
											SELECT t.id 
											FROM ChessTournament t
											WHERE t.name LIKE :name""", 
										  Long.class);
idQuery.setParameter("name", "%Chess%");
idQuery.setFirstResult(0);
idQuery.setMaxResults(5);
List<Long> tournamentIds = idQuery.getResultList();

TypedQuery<ChessTournament> tournamentQuery = em.createQuery("""
																SELECT t 
																FROM ChessTournament t 
																	LEFT JOIN FETCH t.players
																WHERE t.id IN :ids""", 
															 ChessTournament.class);
tournamentQuery.setParameter("ids", tournamentIds);
List<ChessTournament> tournaments = tournamentQuery.getResultList();
tournaments.forEach(t -> log.info(t));

Det forrige kodestykke bruger Hibernate 6. Hvis du bruger Hibernate 5, skal du tilføje DISTINCT søgeord til din 2. forespørgsel, og indstil tippet hibernate.query.passDistinctThrough til false . Som jeg forklarede i en tidligere artikel om Hibernate-indstilling, forhindrer dette Hibernate i at returnere en reference til en ChessTournament objekt for hver af dens spillere.

TypedQuery<Long> idQuery = em.createQuery("""
												SELECT t.id 
												FROM ChessTournament t
												WHERE t.name LIKE :name""", 
											   Long.class);
idQuery.setParameter("name", "%Chess%");
idQuery.setFirstResult(0);
idQuery.setMaxResults(5);
List<Long> tournamentIds = idQuery.getResultList();

TypedQuery<ChessTournament> tournamentQuery = em.createQuery("""
												SELECT DISTINCT t 
												FROM ChessTournament t 
													LEFT JOIN FETCH t.players
												WHERE t.id IN :ids""", 
											   ChessTournament.class);
tournamentQuery.setParameter("ids", tournamentIds);
tournamentQuery.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);
List<ChessTournament> tournaments = tournamentQuery.getResultList();

Denne tilgang ser måske mere kompleks ud og udfører 2 sætninger i stedet for 1, men den adskiller pagineringen af ​​forespørgslens resultatsæt fra initialiseringen af ​​spillerne forening. Dette gør det muligt for Hibernate at tilføje pagineringen til den første forespørgselssætning og forhindrer den i at hente hele resultatsættet og anvende pagineringen i hukommelsen. Det løser advarslen og forbedrer din applikations ydeevne, hvis du arbejder med en enorm database.

07:30:04,557 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id 
    from
        ChessTournament c1_0 
    where
        c1_0.name like ? escape '' offset ? rows fetch first ? rows only
07:30:04,620 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        c1_0.endDate,
        c1_0.name,
        p1_0.ChessTournament_id,
        p1_1.id,
        p1_1.birthDate,
        p1_1.firstName,
        p1_1.lastName,
        p1_1.version,
        c1_0.startDate,
        c1_0.version 
    from
        ChessTournament c1_0 
    left join
        (ChessTournament_ChessPlayer p1_0 
    join
        ChessPlayer p1_1 
            on p1_1.id=p1_0.players_id) 
                on c1_0.id=p1_0.ChessTournament_id 
        where
            c1_0.id in(?,?,?)
07:30:04,666 INFO  [com.thorben.janssen.sample.TestSample] - ChessTournament [id=1, name=Tata Steel Chess Tournament 2021, startDate=2021-01-14, endDate=2021-01-30, version=0]
07:30:04,666 INFO  [com.thorben.janssen.sample.TestSample] - ChessTournament [id=2, name=Tata Steel Chess Tournament 2022, startDate=2022-01-14, endDate=2022-01-30, version=0]
07:30:04,666 INFO  [com.thorben.janssen.sample.TestSample] - ChessTournament [id=3, name=2022 Superbet Chess Classic Romania, startDate=2022-05-03, endDate=2022-05-15, version=0]

Konklusion

Du skal bruge JOIN FETCH klausuler eller EntityGraphs for at initialisere de associationer, du bruger i din virksomhedskode. Dette undgår n+1 udvalgte problemer og forbedrer din applikations ydeevne.

Men hvis du vil begrænse størrelsen af ​​resultatsættet ved at kalde setFirstResult og setMaxResult metoder, skaber hentning af tilknyttede enheder et problem. Resultatsættet indeholder så kombinationen af ​​alle matchende poster i de sammenføjede tabeller. Hvis Hibernate begrænsede størrelsen af ​​det resultatsæt, ville det begrænse antallet af kombinationer i stedet for antallet af valgte enheder. Den henter i stedet hele resultatsættet og anvender pagineringen i hukommelsen. Afhængigt af størrelsen af ​​resultatsættet kan dette forårsage alvorlige ydeevneproblemer.

Det kan du undgå ved at udføre 2 forespørgselssætninger. Den første anvender paginering, når den henter de primære nøgler for alle poster, du vil hente. I eksemplet med dette indlæg var disse id værdier for alle ChessTournament enheder, der matchede WHERE-klausulen. Den 2. forespørgsel bruger derefter listen over primære nøgleværdier til at hente entitetsobjekterne og initialiserer de nødvendige tilknytninger.


Java tag