Max_fetch_depth in depth – Hibernate

Jakob Bendsen har skrevet den sidste artikel i vores JPA føljeton og går i dybden med parameteren max_fetch_depth, som bruges i Hibernates konfigurationsfil til at styre den globale fetch-strategi. Jakob viser hvilken indvirkning max_fetch_depth har, når man bruger klassisk Hibernate, HQL-forespørgsler, samt i JPA med Hibernate som provider. De tre scenarier belyses ved konkrete kodeeksempler og Jakob diskuterer performance for de forskellige fetch-strategier.
I artikelserien om JPA i Lund&Bendsens nyhedsbrevhar Kenn Sano behandlet to typiske performance killers, nemlig N+1-selects problemet og Cartesian Product problemet. Et relateret problem, som jeg er stødt på i JPA/Hibernate-projekter, handler om styring af den globale fetch-strategi i Hibernate og JPA med Hibernate som provider. Hibernate giver mulighed for at konfigurere den globale fetch-politik ved hjælp af parameteren max_fetch_depth i konfigurationsfilen hibernate-cfg.xml. Denne parameter er imidlertid ikke specielt godt dokumenteret, og derfor oplever jeg ofte, at selv erfarne Hibernate-brugere er i tvivl om hvad max_fetch_depth præcis gør, og hvornår den har betydning. I denne artikel stiller jeg derfor skarpt på max_fetch_depth i såvel klassisk Hibernate som i JPA med Hibernate som underliggende implementation.

Lad os starte med at se på dokumentationen for max_fetch_depth:

Sets a maximum “depth” for the outer join fetch tree for single-ended associations (one-to-one, many-to-one). A 0 disables default outer join fetching. E.g. recommended values between 0 and 3.

OK, det handler tilsyneladende om hvor mange outer left joins Hibernate må lave, men jeg kan godt forstå, at betydningen af max_fetch_depth ikke nødvendigvis står én lysende klart selv efter en nærlæsning af Hibernate-doc’en.

For at blive klogere må vi derfor i gang med at eksperimentere selv og da det handler om single-ended associationer (one-to-one og én-siden af one-to-many), laver vi en model med tre one-to-one-associationer givet ved følgende klassediagram:

Hibernate fetch_depth_klasser

Felterne med navnet id er defineret som Hibernate ID’er (dvs. primærnøgler).

Vi lader Hibernate generere tabellerne og indsætter derefter to poster i hver tabel, så vi ender med følgende tabeludseende (ikke udfyldte felter svarer til null-værdier):

Hibernate fetch_depth_tabeller

Dette setup benyttes i de efterfølgende eksperimenter.

Max_fetch_depth i klassisk Hibernate

Vi starter med at definere samtlige associationer så simpelt som overhovedet muligt. For nemheds skyld angiver vi dog cascading=all, således at fx save-operationer propageres transitivt.

A.hbn.xml

<many-to-one name="b1" unique="true" column="b1_id" 
                               cascade="all"/>

På tilsvarende vis defineres associationer for B1.hbn.xml og B2.hbn.xml. Bemærk, at man i Hibernate laver one-to-one-associationer ved at bruge many-to-one og kombinere den med unique=”true”, som betyder at fremmednøglen skal være unik, hvorved man reelt opnår one-to-one-semantik. I Hibernate findes også en one-to-one-konstruktion, men den bruges til noget andet, som jeg ikke vil komme nærmere ind på her.

Når vi nu indlæser en A-entitet med linien:

A a = (A) session.get(A.class,new Integer(1));

anvendes Hibernates standard-adfærd for single-ended associationer. Denne definerer en doven (lazy) indlæsningspolitik for associerede objekter, dvs. at vi får indlæst A-objektet, men IKKE det associerede B1-objekt. I ovenstående scenarie sker der derfor ikke nogen outer join fetching overhovedet, og max_fetch_depth har dermed ingen betydning.

For at se max_fetch_depth i aktion bliver vi nødt til at definere en join-baseret indlæsningspolitik for associationerne. Det kan vi gøre ved at ændre A-, B1-, og B2-hbn.xml-filerne, således at associationerne får en fetch=”join”-attribut. Her ses et eksempel fra A.hbn.xml:

<many-to-one name="b1" unique="true" column="b1_id"
         cascade="all" fetch="join"/>

Vi har nu angivet, at Hibernate skal benytte outer left join SQL-konstruktion til at indlæse associerede objekter. Da vi sætter fetch=”join” på samtlige tre associationer burde resultatet af en indlæsning af A nu være, at B1, B2 og B3 også indlæses, dvs. at indlæsningsstrategien er grådig (eager). Vi prøver igen at indlæse A med:

A a = (A) session.get(A.class,new Integer(1));

Følgende SQL sendes til basen (her vist i en simplificeret form):

select a.*, b1.*, b2.*, b3.* from a 
    left outer join b1 on a.b1_id=b1.id 
        left outer join b2 on b1.b2_id=b2.id 
            left outer join b3 on b3.id=b2.b3_id.id where a.id=?

Vi ser, at vi her får joinet alle fire tabeller vha. tre left outer joins.

Lad os nu introducere max_fetch_depth i hibernate.cfg.xml og se hvilken effekt det har. Vi starter med at sætte den til 1, således:

<property name="max_fetch_depth" >1</property>

En gentagelse af kørslen viser nu følgende SQL-sætning:

select a.*, b1.* from a left outer join b1 on a.b1_id=b1.id where a.id=?

og det er først når vi eksplicit navigerer til et B2-objekt, fx med sætningen:

B2 b2 = a.getB1().getB2();

at de resterende objekter indlæses:

select b2.*, b3.* from b2
select b2.*, b3.* from b2 
    left outer join b3 on b2.b3_id=b3.id where b2.id=?
left outer join b3 on b2.b3_id=b3.id where b2.id=?

En max_fetch_depth på 1 begrænser mao. antallet af outer left joins til 1 pr. SQL-sætning. Øger vimax_fetch_depth til 2 og gentager kørslen produceres nu følgende 2 SQL-sætning:

select a.*,b1.*, b2.* from a left outer join b1 on a.b1_id=b1.id 
        left outer join b2 on b1.b2_id=b2.id where a0_.id=?

Og når vi rent faktisk tilgår et B3-objekt, fx ved

b2.getB3().getS();

eksekveres:

select b3.* from b3 where b3.id=?

Hibernate må nu lave to joins pr sætning og den første sætning indlæser derfor A, B1 og B2 og vi mangler nu kun B3-objektet, som indlæses med den sidste sætning.

Vores foreløbige konklusion er derfor, at max_fetch_depth kan bruges til at begrænse antallet af outer left joins, som Hibernate må udføre ved eager indlæsning af objekter associeret med fetch=”join”-associationer.

HQL og max_fetch_depth

Hibernates HQL-sprog (og i øvrigt også Criteria-API’et) tillader at man laver eksplicitte outer left joins ved at bruge konstruktionen left join fetch, fx from A as a left join fetch a.b1 as b1. Hvordan påvirkermax_fetch_depth HQL-forespørgsler?

Vi afprøver dette med følgende simple kodestump:

Query query = session.createQuery(
    "from A as a left join fetch a.b1 as b1 
        left join fetch b1.b2 as b2 left join fetch b2.b3 as b3");
List list = query.list();

Max_fetch_depth sættes til 1 og HQL-forespørgslen resulterer i følgende SQL:

select a.*, b1.*, b2.*, b3.* from a 
    left outer join b1 on a.b1_id=b1.id 
        left outer join b2 on b1.b2_id=b2.id 
            left outer join b3 on b3.id=b2.b3_id.id where a.id=?  

Max_fetch_depth forsøges også sat til andre værdier, men resultatet er det samme. Konklusionen er, at max_fetch_depth ikke har nogen betydning for HQL-forespørgsler, som benytter left outer joins. Det giver god mening, idet man med HQL selv tager ansvaret for, hvor mange joins der skal udføres. Ønsker man at begrænse antallet af joins, må man bryde en forespørgsel op i flere HQL-sætninger.
I tilfældet med fetch=”join” på associationer kan man derimod, ved blot at navigere rundt i objektgrafen, uforvarent komme til at affyre vilkårligt dybe left outer join-select-sætninger og da disse kan være meget tunge, kan det give god mening at definere en global øvre grænse for dybden.

Max_fetch_depth og JPA

Max_fetch_depth er en Hibernate-specifik-feature, men da Hibernate kan bruges som JPA-provider (dvs. som underliggende implementation for JPA) er det også interessant at undersøge hvilken effekt max_fetch_depth har i JPA.

Vi benytter samme datamodel som før, og vælger at benytte JPAs annotationer til angivelse af meta-data:

@Entity
public class A {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    private String a1;
    private String a2;
    @OneToOne(cascade=CascadeType.ALL) 
        private B1 b1;

    //constructors, getters og setters
}

Vi starter med at udelade max_fetch_depth fra konfigurationsfilen persistence.xml.
På samme tabellayout og indhold som før, prøver vi nu at køre følgende:

EntityManager em = …
A a = em.find(A.class, new Integer(1));

Det resulterer i følgende SQL til basen:

select a.*, b1.*, b2.*, b3.* from a 
    left outer join b1 on a.b1_id=b1.id 
        left outer join b2 on b1.b2_id=b2.id 
            left outer join b3 on b3.id=b2.b3_id.id where a.id=?

Det passer glimrende med, at JPA-specifikationen (sektion 9.1.23) definerer en eager default indlæsningsstrategi for single-ended associationer – i modsætning til Hibernate, som default er lazy for alle associationstyper.

Vi prøver nu at indsætte max_fetch_depth=0 i persistence.xml-filen, såldes:

<property name="hibernate.max_fetch_depth" value="0" />

Resultatet er nu:

select * from a where a.id=?

Og navigerer vi eksplicit ud ad associationerne indlæses de relaterede objekter eet ad gangen:

a.getB1().getB2().getB3().getS();

giver følgende:

select * from b1 where id=?
select * from b2 where id=?
select * from b3 where id=?

Ændrer vi max_fetch_depth til 1 forventer vi, at der nu kun udføres to select-sætninger, som til gengæld joiner hhv A og B1 sammen og B2 og B3. Vi forsøger:

<property name="hibernate.max_fetch_depth" value="1" />

Og får det forventede resultat:

select a.*, b1.* from a left outer join b1 on a.b1_id=b1.id where a.id=?

select b2.*, b3.* from b2 
    left outer join b3 on b2.b3_id=b3.id where b2.id=?

Konklusionen er således, at max_fetch_depth i JPA har samme semantik som i Hibernate, nemlig at bestemme den maksimale dybde af outer left joins. Eftersom JPAs standardadfærd er at indlæse single-ended associationer grådigt, kan det være særdeles relevant at definere en fornuftig max_fetch_depth i JPA-applikationer. Alternativt risikerer man at få udført meget dybe left outer join-sætninger i basen, hvilket kan skade performance. Hibernate-dokumentationen foreslår værdier mellem 0 og 3 og det er givet et glimrende valg i de fleste situationer.

Konklusion

Vi har gennemført en række eksperimenter med max_fetch_depth i klassisk Hibernate og JPA. I klassisk Hibernate kan vi se, at når vi benytter single-ended associationer med fetch=”join” vil værdien af max_fetch_depth begrænse antallet af left outer joins pr. SQL-sætning. For HQL-forespørgsler som eksplicit benytter left join fetch-faciliteten har max_fetch_depth ingen effekt. I JPA kan det give rigtig god mening at definere en fornuftig værdi for max_fetch_depth, da JPA som standard indlæser single-ended-associerede objekter eagerly.

Jakob Bendsen Profil

Jakob Bendsen

Chefkonsulent | Partner

Jakob har arbejdet professionelt med it og softwareudvikling siden 1990’erne. Java og JVM frameworks (Java EE, Spring…) er hans hjemmebane, men også cloud, virtualisering og integrationteknologi (fx REST og GraphQL) er i værktøjskassen. Han er specialist indenfor facilitering af effektive udviklingsprocesser og enterprisearkitektur. 20+ års erfaring med undervisning, mentoring og rådgivning indenfor softwareudvikling i mange brancher, og altid med øjet rettet mod samspillet mellem forretning og IT.