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

従業員は、1つの部門にのみ割り当てられます。

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

最後に、各従業員には複数の電話があります。

@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. 単一値の連想ナビゲーションによる暗黙の内部結合

内部結合は暗黙的である可能性があります。名前が示すように、開発者は暗黙的内部結合を指定しません。 単一値の関連付けをナビゲートするときはいつでも、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の関係にあります。 e.departmentを指定して、従業員エンティティからその部門に移動する場合、単一値の関連付けを移動します。 その結果、JPAは内部結合を作成します。 さらに、結合条件はマッピングメタデータから導出されます。

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

次に、明示的な内部結合を確認します。ここで、 JPQLクエリでJOINキーワードを使用します:

@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のみを選択する場合はそしてe.departmentのようなパス式を使用しないので、クエリでJOINキーワードを使用する必要があります。

第二に、私たちが明示的である場合、何が起こっているのかを知ることはより簡単になる可能性があります。

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

明示する必要があるもう1つの場所は、コレクション値の関連付けです。

データモデルを見ると、従業員電話と1対多の関係にあります。 前の例のように、同様のクエリを作成してみることができます。

SELECT e.phones FROM Employee e

ただし、意図したとおり、これは完全には機能しません。 選択した関連付けe.phones、はコレクション値であるため、電話エンティティの代わりにコレクションのリストを取得します。

@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が関連付けられているDepartmentと、関連付けられていないDepartmentが含まれます。

これは、左外部結合とも呼ばれます。 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つのエンティティを使用してきましたが、これは規則ではありません。 単一の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の関連付けを熱心にロードします。

@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つあります。 従業員は熱心にロードされています。 これは、上記のテストで getResultList を呼び出すと、 Departmentエンティティにemployeesフィールドが読み込まれるため、データベースへの別のアクセスが節約されることを意味します。

ただし、メモリのトレードオフに注意する必要があります。 クエリを1つだけ実行したため、効率が向上する可能性がありますが、すべてのDepartmentとその従業員を一度にメモリにロードしました。

外部結合と同様の方法で外部フェッチ結合を実行することもできます。外部結合では、結合条件に一致しない左側のエンティティからレコードを収集します。 さらに、指定された関連付けを熱心にロードします。

@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から入手できます。