Tipi di join JPA

1. Panoramica

In questo tutorial, esamineremo diversi tipi di join supportati da JPA.

A tale scopo, utilizzeremo JPQL, un linguaggio di query per JPA.

2. Modello di dati di esempio

Diamo un'occhiata al nostro modello di dati di esempio che useremo negli esempi.

Innanzitutto, creeremo un'entità Employee :

@Entity public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String name; private int age; @ManyToOne private Department department; @OneToMany(mappedBy = "employee") private List phones; // getters and setters... }

Ogni Dipendente verrà assegnato a un solo Dipartimento :

@Entity public class Department { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; @OneToMany(mappedBy = "department") private List employees; // getters and setters... }

Infine, ogni dipendente avrà più telefoni :

@Entity public class Phone { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String number; @ManyToOne private Employee employee; // getters and setters... }

3. Inner Joins

Inizieremo con i join interni. Quando due o più entità sono unite internamente, nel risultato vengono raccolti solo i record che corrispondono alla condizione di join.

3.1. Inner Join implicito con navigazione di associazione a valore singolo

I join interni possono essere impliciti. Come suggerisce il nome, lo sviluppatore non specifica inner join impliciti . Ogni volta che navighiamo in un'associazione a valore singolo, JPA crea automaticamente un join implicito:

@Test public void whenPathExpressionIsUsedForSingleValuedAssociation_thenCreatesImplicitInnerJoin() { TypedQuery query = entityManager.createQuery( "SELECT e.department FROM Employee e", Department.class); List resultList = query.getResultList(); // Assertions... }

In questo caso, l' entità Employee ha una relazione molti-a-uno con l' entità Department . Se navighiamo da un'entità Dipendente al suo dipartimento , specificando e.department, navigheremo in un'associazione a valore singolo. Di conseguenza, JPA creerà un inner join. Inoltre, la condizione di join verrà derivata dalla mappatura dei metadati.

3.2. Join interno esplicito con associazione a valore singolo

Successivamente, esamineremo gli inner join espliciti in cui utilizziamo la parola chiave JOIN nella nostra query JPQL:

@Test public void whenJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Employee e JOIN e.department d", Department.class); List resultList = query.getResultList(); // Assertions... }

In questa query, abbiamo specificato una parola chiave JOIN e l' entità Department associata nella clausola FROM , mentre nella precedente non erano specificate affatto. Tuttavia, a parte questa differenza sintattica, le query SQL risultanti saranno molto simili.

Possiamo anche specificare una parola chiave INTERNA facoltativa:

@Test public void whenInnerJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Employee e INNER JOIN e.department d", Department.class); List resultList = query.getResultList(); // Assertions... }

Quindi, poiché JPA sarà implicitamente in un join interno, quando dovremmo essere espliciti?

In primo luogo, JPA crea un inner join implicito solo quando specifichiamo un'espressione di percorso. Ad esempio, quando vogliamo selezionare solo i dipendenti che hanno un dipartimento e non usiamo un'espressione di percorso - e.department - , dovremmo usare la parola chiave JOIN nella nostra query.

In secondo luogo, quando siamo espliciti, può essere più facile sapere cosa sta succedendo.

3.3. Join interno esplicito con associazioni con valore di raccolta

Un altro punto in cui dobbiamo essere espliciti è con le associazioni a valore di raccolta.

Se guardiamo al nostro modello di dati, il dipendente ha una relazione uno-a-molti con Phone . Come in un esempio precedente, possiamo provare a scrivere una query simile:

SELECT e.phones FROM Employee e

Ma questo non funzionerà come forse intendevamo. Poiché l'associazione selezionata, e.phones , ha un valore di raccolta, otterremo un elenco di raccolte anziché di entità telefono :

@Test public void whenCollectionValuedAssociationIsSpecifiedInSelect_ThenReturnsCollections() { TypedQuery query = entityManager.createQuery( "SELECT e.phones FROM Employee e", Collection.class); List resultList = query.getResultList(); //Assertions }

Inoltre, se vogliamo filtrare le entità Phone nella clausola WHERE, JPA non lo consentirà. Questo perché un'espressione di percorso non può continuare da un'associazione con valori di raccolta . Quindi, ad esempio, e.phones.number non è valido .

Dovremmo invece creare un inner join esplicito e creare un alias per l' entità Phone . Quindi possiamo specificare l' entità Phone nella clausola SELECT o WHERE:

@Test public void whenCollectionValuedAssociationIsJoined_ThenCanSelect() { TypedQuery query = entityManager.createQuery( "SELECT ph FROM Employee e JOIN e.phones ph WHERE ph LIKE '1%'", Phone.class); List resultList = query.getResultList(); // Assertions... }

4. Join esterno

Quando due o più entità sono outer- join , i record che soddisfano la condizione di join e anche i record nell'entità sinistra vengono raccolti nel risultato:

@Test public void whenLeftKeywordIsSpecified_thenCreatesOuterJoinAndIncludesNonMatched() { TypedQuery query = entityManager.createQuery( "SELECT DISTINCT d FROM Department d LEFT JOIN d.employees e", Department.class); List resultList = query.getResultList(); // Assertions... }

Qui, il risultato conterrà i dipartimenti a cui sono associati i dipendenti e anche quelli che non ne hanno.

Viene anche definito join esterno sinistro. JPA non fornisce giunti corretti in cui raccogliamo anche record non corrispondenti dall'entità giusta. Anche se possiamo simulare giunti giusti scambiando entità nella clausola FROM.

5. Si unisce alla clausola WHERE

5.1. Con una condizione

Possiamo elencare due entità nella clausola FROM e quindi specificare la condizione di join nella clausola WHERE .

This can be handy especially when database level foreign keys aren't in place:

@Test public void whenEntitiesAreListedInFromAndMatchedInWhere_ThenCreatesJoin() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Employee e, Department d WHERE e.department = d", Department.class); List resultList = query.getResultList(); // Assertions... }

Here, we're joining Employee and Department entities, but this time specifying a condition in the WHERE clause.

5.2. Without a Condition (Cartesian Product)

Similarly, we can list two entities in the FROM clause without specifying any join condition. In this case, we'll get a cartesian product back. This means that every record in the first entity is paired with every other record in the second entity:

@Test public void whenEntitiesAreListedInFrom_ThenCreatesCartesianProduct() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Employee e, Department d", Department.class); List resultList = query.getResultList(); // Assertions... }

As we can guess, these kinds of queries won't perform well.

6. Multiple Joins

So far, we've used two entities to perform joins, but this isn't a rule. We can also join multiple entities in a single JPQL query:

@Test public void whenMultipleEntitiesAreListedWithJoin_ThenCreatesMultipleJoins() { TypedQuery query = entityManager.createQuery( "SELECT ph FROM Employee e JOIN e.department d JOIN e.phones ph WHERE d.name IS NOT NULL", Phone.class); List resultList = query.getResultList(); // Assertions... }

Here, we're selecting all Phones of all Employees that have a Department. Similar to other inner joins, we're not specifying conditions since JPA extracts this information from mapping metadata.

7. Fetch Joins

Now, let's talk about fetch joins. Its primary usage is for fetching lazy-loaded associations eagerly for the current query.

Here, we'll eagerly load Employees association:

@Test public void whenFetchKeywordIsSpecified_ThenCreatesFetchJoin() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Department d JOIN FETCH d.employees", Department.class); List resultList = query.getResultList(); // Assertions... }

Although this query looks very similar to other queries, there is one difference, and that is that the Employees are eagerly loaded. That means that once we call getResultList in the test above, the Department entities will have their employees field loaded, thus saving us another trip to the database.

But be aware of the memory trade-off. We may be more efficient because we only performed one query, but we also loaded all Departments and their employees into memory at once.

Possiamo anche eseguire il join di recupero esterno in modo simile ai join esterni, in cui raccogliamo i record dall'entità sinistra che non corrispondono alla condizione di join. Inoltre, carica con entusiasmo l'associazione specificata:

@Test public void whenLeftAndFetchKeywordsAreSpecified_ThenCreatesOuterFetchJoin() { TypedQuery query = entityManager.createQuery( "SELECT d FROM Department d LEFT JOIN FETCH d.employees", Department.class); List resultList = query.getResultList(); // Assertions... }

8. Riepilogo

In questo articolo, abbiamo trattato i tipi di join JPA.

Come sempre, puoi controllare tutti gli esempi per questo e altri tutorial su GitHub.