Типове JPA присъединяване

1. Общ преглед

В този урок ще разгледаме различни типове съединения, поддържани от JPA.

За тази цел ще използваме JPQL, език за заявки за JPA.

2. Примерен модел на данни

Нека разгледаме нашия примерен модел на данни, който ще използваме в примерите.

Първо, ще създадем обект на служител :

@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... }

Всеки служител ще бъде назначен само в един отдел :

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

И накрая, всеки служител ще има няколко телефона :

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

3. Вътрешни присъединявания

Ще започнем с вътрешни съединения. Когато две или повече обекти са вътрешно съединени, в записа се събират само записите, които съответстват на условието за присъединяване.

3.1. Неявно вътрешно присъединяване с еднозначна навигация за асоцииране

Вътрешните съединения могат да бъдат неявни. Както подсказва името, разработчикът не посочва неявни вътрешни съединения . Винаги, когато навигираме в еднозначна асоциация, JPA автоматично създава неявно присъединяване:

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

Тук обектът „ Служител“ има връзка „много към едно“ с обекта на отдела . Ако се придвижваме от обект на служител до нейния отдел - посочвайки e.department - ще се ориентираме в еднозначна асоциация. В резултат на това JPA ще създаде вътрешно съединение. Освен това условието за присъединяване ще бъде получено от метаданни за картографиране.

3.2. Изрично вътрешно присъединяване с еднозначна асоциация

След това ще разгледаме явни вътрешни съединения, където използваме ключовата дума JOIN в нашата 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... }

В тази заявка посочихме ключова дума JOIN и свързания обект на отдел в клаузата FROM , докато в предишната изобщо не бяха посочени. Въпреки това, освен тази синтактична разлика, получените SQL заявки ще бъдат много сходни.

Можем също така да посочим незадължителна ключова дума INNER:

@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... }

И така, тъй като JPA имплицитно ще се присъедини към вътрешно присъединяване, кога ще трябва да бъдем изрични?

Първо, JPA създава имплицитно вътрешно присъединяване само когато зададем израз на път. Например, когато искаме да изберем само служителите , които имат отдел и не използваме израз на път - напр. Отдел - , трябва да използваме ключова дума JOIN в нашата заявка.

Второ, когато сме изрични, може да е по-лесно да знаем какво става.

3.3. Изрично Вътрешно присъединяване с асоциирани с колекция ценности

Друго място, на което трябва да бъдем категорични, е асоциациите, оценявани от колекцията.

Ако разгледаме нашия модел данни, Служителят има връзка едно към много с Телефон . Както в по-ранен пример, можем да се опитаме да напишем подобна заявка:

SELECT e.phones FROM Employee e

Но това няма да проработи съвсем, както може би сме възнамерявали. Тъй като избраната асоциация - д. Телефони - се оценява от колекцията, ще получим списък от Колекции , вместо Телефонни обекти :

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

Освен това, ако искаме да филтрираме телефонни обекти в клаузата WHERE, JPA няма да позволи това. Това е така, защото изразът на път не може да продължи от асоциирана с колекция стойност . Така например, e.phones.number не е валиден .

Вместо това трябва да създадем изрично вътрешно присъединяване и да създадем псевдоним за обекта Телефон . След това можем да посочим обекта Телефон в клаузата SELECT или 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. Външно присъединяване

Когато две или повече обекти са обединени отвън, в резултата се събират записите, които отговарят на условието за присъединяване, както и записите в левия обект:

@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... }

Тук резултатът ще съдържа отдели , които имат асоциирани служители , както и тези, които нямат такива.

Това също се нарича ляво външно съединение. JPA не предоставя правилни присъединявания, където също събираме несъвпадащи записи от правилния обект. Въпреки че можем да симулираме правилните присъединявания чрез размяна на обекти в клаузата FROM.

5. Присъединява се към клаузата WHERE

5.1. Със Състояние

Можем да изброим две обекти в клаузата FROM и след това да посочим условието за присъединяване в клаузата 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.

Също така можем да изпълним външното извличане на присъединяване по подобен начин на външното обединяване, където събираме записи от левия обект, които не отговарят на условието за присъединяване. И освен това с нетърпение зарежда посочената асоциация:

@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. Обобщение

В тази статия разгледахме типовете JPA присъединяване.

Както винаги, можете да разгледате всички проби за този и други уроци в GitHub.