JPA結合タイプ

1. 概要

このチュートリアルでは、https://www.baeldung.com/jpa-hibernate-difference [JPA]でサポートされているさまざまな結合タイプを見ていきます。
そのために、JPQL link:/jpa-queries[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...
}
各_Employee_は、1つの_Department_のみに割り当てられます。
@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. 内部結合

内部結合から始めます。 2つ以上のエンティティが内部結合されている場合、結合条件に一致するレコードのみが結果に収集されます。

3.1. 単一値の関連付けナビゲーションによる暗黙的な内部結合

内部結合には_implicit._を使用できます。名前が示すように、*開発者は暗黙的な内部結合を指定しません*。 単一値の関連付けをナビゲートするたびに、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...
}
ここで、_Employee_エンティティは、_Department_エンティティと多対1の関係にあります。 * _Employee_エンティティから彼女の_Department_に移動する場合(_e.departmentを指定する場合)には、単一値の関連付けを移動します。 その結果、JPAは内部結合を作成します。 さらに、結合条件はメタデータのマッピングから導出されます。

3.2. 単一値の関連付けによる明示的な内部結合

次に、https://www.baeldung.com/jpa-queries [JPQLクエリ]でJOINキーワードを使用する_explicit_内部結合を確認します。*
@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...
}
このクエリでは、* FROM句で* JOINキーワードと関連する_Department_エンティティを指定しましたが、前のものではまったく指定されていませんでした。 ただし、この構文の違いを除いて、結果の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は、パス式を指定した場合にのみ暗黙的な内部結合を作成します。*たとえば、_Department_を持つ__Employee__sのみを選択し、パス式を使用しない場合– _e.department – 「_、クエリでJOINキーワードを使用する必要があります。
第二に、明示的であれば、何が起こっているのかを簡単に知ることができます。

3.3. コレクション値の関連付けによる明示的な内部結合

*明示的にする必要があるもう1つの場所は、コレクション値の関連付けです。*
データモデルを見ると、_Employee_は_Phone_と1対多の関係にあります。 前の例のように、同様のクエリを書くことができます。
SELECT e.phones FROM Employee e
*ただし、これはうまくいきません*意図したとおりです。 選択された関連付け-_e.phones_-はコレクション値であるため、* _ Phone_エンティティの代わりに__Collection__sのリストを取得します*:
@Test
public void whenCollectionValuedAssociationIsSpecifiedInSelect_ThenReturnsCollections() {
    TypedQuery<Collection> query
      = entityManager.createQuery("SELECT e.phones FROM Employee e", Collection.class);
    List<Collection> resultList = query.getResultList();

    //Assertions
}
さらに、WHERE句で_Phone_エンティティをフィルタリングする場合、JPAはそれを許可しません。 これは、*パス式がコレクション値の関連付けから継続できないためです*。 したがって、たとえば、* _ e.phones.number_は無効です*。
代わりに、明示的な内部結合を作成し、_Phone_エンティティのエイリアスを作成する必要があります。 その後、SELECTまたはWHERE句で_Phone_エンティティを指定できます。
@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. 外部結合

2つ以上のエンティティが外部結合されている場合、*結合条件を満たすレコードと左側のエンティティのレコードも結果に収集されます:*
@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...
}
この場合、結果には__Employee__sに関連付けられた__Department__sと、関連付けられていない__Department__sが含まれます。
これは、左外部結合とも呼ばれます。 * JPAは、適切なエンティティから不一致のレコードも収集する適切な結合*を提供しません。 ただし、FROM句のエンティティを交換することにより、正しい結合をシミュレートできます。

5. WHERE句で結合する

5.1. 条件付き

  • FROM句に2つのエンティティをリストし、* * 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句に2つのエンティティをリストできます*。 この場合、*デカルト積が返されます*。 つまり、最初のエンティティのすべてのレコードは、2番目のエンティティの他のすべてのレコードとペアになります。
@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. 複数の結合

これまで、2つのエンティティを使用して結合を実行しましたが、これはルールではありません。 * 1つの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...
}
ここでは、_Departmentを持つすべての_Employees_のすべての_Phones_を選択しています。他の内部結合と同様に、JPAはマッピングメタデータからこの情報を抽出するため、条件を指定していません。

7. フェッチ結合

次に、フェッチ結合について説明します。 *主な用途は、現在のクエリのlink:/hibernate-lazy-eager-loading [遅延読み込みされたアソシエーションを積極的に取得する] *です。
ここでは、__ Employee__s associationを積極的にロードします。
@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...
}
このクエリは他のクエリと非常によく似ていますが、1つの違いがあり、それは* __ Employee__sが積極的にロードされている*ことです。 つまり、上記のテストで_getResultList_を呼び出すと、_Department_エンティティの_employees_フィールドがロードされるため、データベースへの別の旅行が節約されます。
*ただし、メモリのトレードオフに注意してください*。 クエリを1つしか実行しなかったため、より効率的かもしれませんが、すべての__Department__sとその従業員を一度にメモリにロードしました。
外部結合と同様の方法で外部フェッチ結合を実行することもできます。この場合、結合条件に一致しないレコードを左側のエンティティから収集します。 さらに、指定された関連付けを積極的にロードします。
@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の結合タイプについて説明しました。
いつものように、このチュートリアルとhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa-2[Github上]のすべてのサンプルをチェックアウトできます。