Java >> Java tutorial >  >> Tag >> class

Den bedste måde at hente en tilknytning defineret af en underklasse

EntityGraphs og JOIN FETCH-klausuler giver en nem og effektiv måde at hente en enhed og initialisere dens tilknytninger. Men hvis du prøver at bruge det med en domænemodel, der bruger arv, vil du hurtigt løbe ind i et problem:

Du kan ikke bruge denne tilgang i en polymorf forespørgsel til at hente en tilknytning, der er defineret på en underklasse. Eller med andre ord, din JOIN FETCH-klausul eller EntityGraph skal referere til en entity-attribut, der er defineret af din superklasse. Ellers vil Hibernate give en undtagelse, fordi attributten er ukendt for nogle underklasser.

Men der er en nem løsning baseret på Hibernates cache på 1. niveau og dens garanti for, at der kun er 1 enhedsobjekt for hver databasepost i en Hibernate-session. Lad os tage et kig på et eksempel, og jeg viser dig, hvordan denne løsning fungerer.

Bemærk:Denne artikel er inspireret af et spørgsmål om StackOverflow, som jeg kunne gøre krav på dusøren for med et svar, jeg forberedte på en kaffe med Thorben livestream.

Domænemodellen

Modellen brugt i denne artikel er enkel. En forfatter kan skrive forskellige typer Publikation s, som Book s og BlogPost s. Disse 2 slags Publicering s deler attributterne id, version, title, publishingDate, og en henvisning til Forfatteren . Blogindlæg s bliver offentliggjort på deres forfatters blog, så de har den ekstra attribut url . Book s kan være udgivet af en udgiver , som jeg modellerede som en reference til en anden enhed i vores lille domænemodel.

Der er ikke noget særligt ved enhedskortlægningerne. Jeg bruger InheritanceType.SINGLE_TABLE for at kortlægge Publikationen , Bog, og BlogPost enheder til den samme databasetabel.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Publication {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	protected Long id;
	
	protected String title;	
	
	@Version
	private int version;
	
	@ManyToOne(fetch = FetchType.LAZY)
	protected Author author;
	
	protected LocalDate publishingDate;

	...
}
@Entity
@DiscriminatorValue("Blog")
public class BlogPost extends Publication {

	private String url;

	...
}

Bogenheden definerer også en en-til-mange-tilknytning til Udgiveren enhed.

@Entity
@DiscriminatorValue("Book")
public class Book extends Publication {

	private int pages;

	@ManyToOne
	private Publisher publisher;

	...
}

InheritanceType.SINGLE_TABLE sætter os i stand til at definere en polymorf en-til-mange-tilknytning kortlægning mellem Forfatteren og Publikationen enhed.

@Entity
public class Author {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;
	
	@Version
	private int version;

	private String firstName;

	private String lastName;

	@OneToMany(mappedBy="author")
	private Set<Publication> publications = new HashSet<Publication>();

	...
}

Hent en forfatter med deres blogindlæg, bøger og udgivere

OK, lad os besvare vores indledende spørgsmål:Hvordan kan du initialisere tilknytningen mellem bogen og Udgiveren enhed, hvis du får en forfatter med hele deres Publikation ?

Hvis du forventede at gøre dette i 1 forespørgsel, må jeg skuffe dig. Hibernate understøtter det ikke. Men ved at bruge følgende løsning behøver du kun 2 forespørgsler. Det er meget bedre end de n+1-forespørgsler, du ville have brug for uden det.

Så hvordan virker det? Som jeg sagde, understøtter Hibernate kun JOIN FETCH-klausuler eller EntityGraphs på attributter, der er defineret af alle entitetsklasser i en polymorf association. Derfor har du brug for en ekstra forespørgsel for at få bogen s med deres udgiver s. I det næste trin skal du derefter genbruge disse objekter, når du behandler resultaterne af den 2. forespørgsel.

Hibernates cache på 1. niveau til undsætning

Ved at bruge Hibernates cache på 1. niveau og dens garanti for, at en databasepost i en Hibernate-session kun bliver kortlagt af 1 enhedsobjekt, kan du implementere dette meget effektivt. Din første forespørgsel får hele bogen enheder og deres udgiver , som du skal bruge til din brugssag.

I dette eksempel er disse alle Book er skrevet af en forfatter med fornavn Thorben. Som du kan se, er forespørgslen ikke for kompleks. Jeg tilmelder mig fra bogen til Forfatteren for at kunne definere WHERE-sætningen, og jeg bruger en JOIN FETCH-sætning til at initialisere tilknytningen mellem bogen og Udgiveren .

Query q1 = em.createQuery("SELECT b FROM Book b JOIN b.author a JOIN FETCH b.publisher p WHERE a.firstName = :fName");

Når Hibernate behandler resultatet af denne forespørgsel, føjer den alle entitetsobjekter til sin cache på 1. niveau. Når den så skal behandle resultatet af en anden forespørgsel, som returnerer Bog entiteter, kontrollerer Hibernate først, om det entitetsobjekt allerede er gemt i cachen på 1. niveau. Hvis det er tilfældet, får den det derfra.

Dette er nøgleelementet i denne løsning. Det giver dig mulighed for i den 2. forespørgsel at ignorere sammenhængen mellem bogen og Udgiveren enhed. Fordi Hibernate vil få alle Book enhedsobjekter fra cachen på 1. niveau, tilknytningen til Udgiveren enhed vil alligevel blive initialiseret.

Her kan du se forespørgslen, der får alle Publicering s af Forfatteren med fornavn Thorben. Takket være arvekortlægningen og den kortlagte en-til-mange-forening er denne forespørgsel meget enkel.

Query q2 = em.createQuery("SELECT p FROM Publication p JOIN p.author a WHERE a.firstName = :fName", Publication.class);

Lad os prøve denne løsning ved hjælp af følgende testcase. Den udfører først de 2 beskrevne forespørgsler og skriver derefter en logmeddelelse for hver hentede Publikation . Hvis Publikation er en bog , indeholder denne logmeddelelse navnet på Udgiveren . Og jeg inkluderede også logmeddelelser, der viser objektreferencen for bogen enhedsobjekter. Dette vil vise dig, at Hibernate altid returnerer den samme objektforekomst for bogen enhed.

Query q1 = em.createQuery("SELECT b FROM Book b JOIN b.author a JOIN FETCH b.publisher p WHERE a.firstName = :fName");
q1.setParameter("fName", "Thorben");
List<Book> bs = q1.getResultList();
for (Book b : bs) {
	log.info(b);
}

Query q2 = em.createQuery("SELECT p FROM Publication p JOIN p.author a WHERE a.firstName = :fName", Publication.class);
q2.setParameter("fName", "Thorben");
List<Publication> ps = q2.getResultList();

for (Publication p : ps) {
	if (p instanceof BlogPost) {
		BlogPost blog = (BlogPost) p;
		log.info("BlogPost - "+blog.getTitle()+" was published at "+blog.getUrl());
	} else {
		Book book = (Book) p;
		log.info("Book - "+book.getTitle()+" was published by "+book.getPublisher().getName());
		log.info(book);
	}
}

Som du kan se i logfilen, udførte Hibernate kun de 2 forventede forespørgsler. Selvom den 2. forespørgsel ikke initialiserede tilknytningen mellem bogen og Udgiveren , den dovent hentede forening er tilgængelig. Som de loggede objektreferencer viser, brugte Hibernate den samme bog enhedsobjekt i resultatet af begge forespørgsler.

12:18:05,504 DEBUG [org.hibernate.SQL] - select book0_.id as id2_1_0_, publisher2_.id as id1_2_1_, book0_.author_id as author_i8_1_0_, book0_.publishingDate as publishi3_1_0_, book0_.title as title4_1_0_, book0_.version as version5_1_0_, book0_.pages as pages6_1_0_, book0_.publisher_id as publishe9_1_0_, publisher2_.name as name2_2_1_ from Publication book0_ inner join Author author1_ on book0_.author_id=author1_.id inner join Publisher publisher2_ on book0_.publisher_id=publisher2_.id where book0_.DTYPE='Book' and author1_.firstName=?
12:18:05,537 INFO  [org.thoughts.on.java.TestJpaInheritance] - [email protected]
12:18:05,551 DEBUG [org.hibernate.SQL] - select publicatio0_.id as id2_1_, publicatio0_.author_id as author_i8_1_, publicatio0_.publishingDate as publishi3_1_, publicatio0_.title as title4_1_, publicatio0_.version as version5_1_, publicatio0_.pages as pages6_1_, publicatio0_.publisher_id as publishe9_1_, publicatio0_.url as url7_1_, publicatio0_.DTYPE as dtype1_1_ from Publication publicatio0_ inner join Author author1_ on publicatio0_.author_id=author1_.id where author1_.firstName=?
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - Book - Hibernate Tips - More than 70 solutions to common Hibernate problems was published by Myself
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - [email protected]
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - BlogPost - Best way to fetch an association defined by a subclass was published at https://thorben-janssen.com/fetch-association-of-subclass/

Konklusion

Som du kan se, kan Hibernates cache på 1. niveau og dens garanti for, at hver session kun bruger 1 enhedsrepræsentation for hver databasepost, bruges til at skabe meget effektive implementeringer.

Og før du begynder at bekymre dig, er denne løsning baseret på veldokumenteret adfærd og nøglefunktioner i JPA og Hibernate. Dette er fremtidssikret, og du behøver ikke bekymre dig om det, når du opdaterer din Dvale-afhængighed.


Java tag