Перейти к основному содержимому

Типы соединения JPA

· 7 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим различные типы соединений, поддерживаемые JPA .

Для этой цели мы будем использовать JPQL, язык запросов для JPA .

2. Образец модели данных

Давайте посмотрим на нашу модель данных, которую мы будем использовать в примерах.

Во-первых, мы создадим сущность 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<Phone> phones;

// getters and setters...
}

Каждый сотрудник будет закреплен только за одним отделом :

@Entity
public class Department {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

private String name;

@OneToMany(mappedBy = "department")
private List<Employee> employees;

// getters and setters...
}

Наконец, у каждого Employee будет несколько Phone s:

@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<Department> query
= entityManager.createQuery(
"SELECT e.department FROM Employee e", Department.class);
List<Department> resultList = query.getResultList();

// Assertions...
}

Здесь сущность « Сотрудник » имеет отношение «многие к одному» с сущностью « Отдел ». Если мы перейдем от сущности Сотрудник к ее Отделу, указав e.department, мы будем перемещаться по ассоциации с одним значением. В результате JPA создаст внутреннее соединение. Кроме того, условие соединения будет получено из метаданных сопоставления.

3.2. Явное внутреннее соединение с однозначной ассоциацией

Далее мы рассмотрим явные внутренние соединения, где мы используем ключевое слово JOIN в нашем запросе JPQL :

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

// Assertions...
}

В этом запросе мы указали ключевое слово JOIN и связанную с ним сущность Department в предложении FROM , тогда как в предыдущем запросе они вообще не были указаны. Однако, если не считать этой синтаксической разницы, результирующие SQL-запросы будут очень похожими.

Мы также можем указать необязательное ключевое слово INNER:

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

// Assertions...
}

Итак, поскольку JPA автоматически создает неявное внутреннее соединение, когда нам нужно быть явным?

Прежде всего, JPA создает неявное внутреннее соединение только тогда, когда мы указываем выражение пути. Например, если мы хотим выбрать только сотрудников , у которых есть отдел, и мы не используем выражение пути, такое как e.department , мы должны использовать ключевое слово JOIN в нашем запросе.

Во-вторых, когда мы являемся явными, может быть легче понять, что происходит.

3.3. Явное внутреннее соединение с ассоциациями на основе коллекции

Еще одно место , где мы должны быть явными, — это ассоциации со значением коллекции.

Если мы посмотрим на нашу модель данных, то у Employee есть отношения «один ко многим» с Phone . Как и в предыдущем примере, мы можем попробовать написать аналогичный запрос:

SELECT e.phones FROM Employee e

Однако это не сработает так, как мы, возможно, предполагали. Поскольку выбранная ассоциация, e.phones, имеет значение коллекции, мы получим список объектов Collection вместо объектов Phone :

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

//Assertions
}

Более того, если мы хотим отфильтровать объекты Phone в предложении WHERE, JPA этого не допустит. Это связано с тем, что выражение пути не может быть продолжено из ассоциации со значением коллекции . Так, например, e.phones.number недействителен .

Вместо этого мы должны создать явное внутреннее соединение и псевдоним для объекта Phone . Затем мы можем указать сущность Phone в предложении SELECT или WHERE:

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

// Assertions...
}

4. Внешнее соединение

Когда два или более объекта объединены внешним соединением, в результате собираются записи, удовлетворяющие условию соединения, а также записи в левом объекте:

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

// Assertions...
}

Здесь результат будет содержать отделы , которые связаны с сотрудниками , а также те, у которых их нет.

Это также называется левым внешним соединением. JPA не обеспечивает правильных объединений , когда мы также собираем несоответствующие записи из нужного объекта. Хотя мы можем смоделировать правое соединение, поменяв местами объекты в предложении FROM.

5. Соединения в предложении WHERE

5.1. С условием

Мы можем перечислить две сущности в предложении FROM, а затем указать условие соединения в предложении WHERE .

Это может быть удобно, особенно когда внешние ключи уровня базы данных отсутствуют:

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

// Assertions...
}

Здесь мы присоединяемся к сущностям Employee и Department , но на этот раз указываем условие в предложении WHERE.

5.2. Без условия (Декартово произведение)

Точно так же мы можем перечислить две сущности в предложении FROM без указания какого-либо условия соединения . В этом случае мы получим обратно декартово произведение . Это означает, что каждая запись в первом объекте связана с каждой другой записью во втором объекте:

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

// Assertions...
}

Как мы можем догадаться, такие запросы не будут работать хорошо.

6. Множественные соединения

До сих пор мы использовали две сущности для выполнения соединений, но это не правило. Мы также можем объединить несколько объектов в одном запросе JPQL :

@Test
public void whenMultipleEntitiesAreListedWithJoin_ThenCreatesMultipleJoins() {
TypedQuery<Phone> 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<Phone> resultList = query.getResultList();

// Assertions...
}

Здесь мы выбираем все телефоны всех сотрудников , у которых есть отдел. Как и в случае с другими внутренними соединениями, мы не указываем условия, поскольку JPA извлекает эту информацию из метаданных сопоставления.

7. Получение соединений

Теперь давайте поговорим о соединениях выборки. Их основное использование — ленивая загрузка ассоциаций для текущего запроса .

Здесь мы с нетерпением загрузим ассоциацию Employee s:

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

// Assertions...
}

Хотя этот запрос очень похож на другие запросы, есть одно отличие; Сотрудники с нетерпением загружаются `` . Это означает, что как только мы вызовем getResultList в приведенном выше тесте, у сущностей отдела будет загружено поле их сотрудников , что избавит нас от еще одного обращения к базе данных.

Однако мы должны помнить о компромиссе с памятью . Мы можем быть более эффективными, потому что мы выполнили только один запрос, но мы также загрузили все отделы и их сотрудников в память одновременно.

Мы также можем выполнять внешнее объединение выборки аналогично внешним соединениям, когда мы собираем записи из левого объекта, которые не соответствуют условию соединения. Кроме того, он жадно загружает указанную ассоциацию:

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

// Assertions...
}

8. Резюме

В этой статье мы рассмотрели типы соединений JPA.

Как всегда, примеры для этого и других руководств доступны на GitHub .