1. 概要

このチュートリアルでは、無関係なエンティティ間でJPAクエリを作成する方法を説明します。

2. Mavenの依存関係

pom.xmlに必要な依存関係を追加することから始めましょう。

まず、 Java PersistenceAPIの依存関係を追加する必要があります。

<dependency>
   <groupId>javax.persistence</groupId>
   <artifactId>javax.persistence-api</artifactId>
   <version>2.2</version>
</dependency>

次に、JavaPersistenceAPIを実装するHibernateORMの依存関係を追加します。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.14.Final</version>
</dependency>

そして最後に、いくつかのQueryDSL依存関係を追加します。 つまり、querydsl-aptおよびquerydsl-jpa

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>4.3.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>4.3.1</version>
</dependency>

3. ドメインモデル

この例のドメインはカクテルバーです。 ここに、データベースに2つのテーブルがあります。

  • メニューテーブルには、バーが販売するカクテルとその価格、および
  • カクテルの作成手順を保存するレシピテーブル

これらの2つのテーブルは、互いに厳密に関連しているわけではありません。 カクテルは、レシピの説明がなくてもメニューに含めることができます。 さらに、まだ販売していないカクテルのレシピを入手することもできます。

この例では、利用可能なレシピがあるメニューのすべてのカクテルを検索します。

4. JPAエンティティ

テーブルを表す2つのJPAエンティティを簡単に作成できます。

@Entity
@Table(name = "menu")
public class Cocktail {
    @Id
    @Column(name = "cocktail_name")
    private String name;

    @Column
    private double price;

    // getters & setters
}
@Entity
@Table(name="recipes")
public class Recipe {
    @Id
    @Column(name = "cocktail")
    private String cocktail;

    @Column
    private String instructions;
    
    // getters & setters
}

メニューテーブルとレシピテーブルの間には、明示的な外部キー制約なしの基本的な1対1の関係があります。 たとえば、 cocktail_name列の値が「Mojito」であるmenu レコードと、cocktail列の値が「Mojito」であるrecipesレコードがあるとします。値が「Mojito」の場合、menuレコードはこのrecipesレコードに関連付けられます。

Cocktail エンティティでこの関係を表すために、さまざまな注釈が付けられたレシピフィールドを追加します。

@Entity
@Table(name = "menu")
public class Cocktail {
    // ...
 
    @OneToOne
    @NotFound(action = NotFoundAction.IGNORE)
    @JoinColumn(name = "cocktail_name", 
       referencedColumnName = "cocktail", 
       insertable = false, updatable = false, 
       foreignKey = @javax.persistence
         .ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private Recipe recipe;
   
    // ...
}

最初の注釈は@OneToOne、であり、Recipeエンティティとの基本的な1対1の関係を宣言します。

次に、レシピフィールドに @NotFound(action = NotFoundAction.IGNORE)Hibernateアノテーションを付けます。 これは、メニューテーブルに存在しないカクテルレシピがある場合に例外をスローしないようにORMに指示します。

カクテルを関連付けられたレシピに関連付けるアノテーションは、@JoinColumnです。 この注釈を使用して、2つのエンティティ間の疑似外部キー関係を定義します。

最後に、externalKeyプロパティを@javax.persistence.ForeignKey(value = ConstraintMode.NO_CONSTRAINT)に設定することにより、JPAプロバイダーに外部キー制約を生成しないように指示します。

5. JPAおよびQueryDSLクエリ

レシピに関連付けられているカクテルエンティティを取得することに関心があるため、関連付けられているレシピに結合することでカクテルエンティティをクエリできます。 エンティティ。

クエリを作成する1つの方法は、JPQLを使用することです。

entityManager.createQuery("select c from Cocktail c join c.recipe")

または、QueryDSLフレームワークを使用して:

new JPAQuery<Cocktail>(entityManager)
  .from(QCocktail.cocktail)
  .join(QCocktail.cocktail.recipe)

目的の結果を得る別の方法は、カクテルレシピエンティティと結合し、 on 句を使用して、クエリの基本的な関係を直接定義することです。

これは、JPQLを使用して実行できます。

entityManager.createQuery("select c from Cocktail c join Recipe r on c.name = r.cocktail")

またはQueryDSLフレームワークを使用して:

new JPAQuery(entityManager)
  .from(QCocktail.cocktail)
  .join(QRecipe.recipe)
  .on(QCocktail.cocktail.name.eq(QRecipe.recipe.cocktail))

6. 1対1の結合ユニットテスト

上記のクエリをテストするための単体テストの作成を始めましょう。 テストケースを実行する前に、データベーステーブルにいくつかのデータを挿入する必要があります。

public class UnrelatedEntitiesUnitTest {
    // ...

    @BeforeAll
    public static void setup() {
        // ...

        mojito = new Cocktail();
        mojito.setName("Mojito");
        mojito.setPrice(12.12);
        ginTonic = new Cocktail();
        ginTonic.setName("Gin tonic");
        ginTonic.setPrice(10.50);
        Recipe mojitoRecipe = new Recipe(); 
        mojitoRecipe.setCocktail(mojito.getName()); 
        mojitoRecipe.setInstructions("Some instructions for making a mojito cocktail!");
        entityManager.persist(mojito);
        entityManager.persist(ginTonic);
        entityManager.persist(mojitoRecipe);
      
        // ...
    }

    // ... 
}

の中に設定メソッド、2つ節約していますカクテルエンティティ、 モヒートそしてそのジントニック。 次に、 レシピ 「モヒート」の作り方カクテル

これで、前のセクションのクエリの結果をテストできます。 モヒートカクテルのみにレシピエンティティが関連付けられていることがわかっているため、さまざまなクエリでモヒートカクテルのみが返されると予想されます。

public class UnrelatedEntitiesUnitTest {
    // ...

    @Test
    public void givenCocktailsWithRecipe_whenQuerying_thenTheExpectedCocktailsReturned() {
        // JPA
        Cocktail cocktail = entityManager.createQuery("select c " +
          "from Cocktail c join c.recipe", Cocktail.class)
          .getSingleResult();
        verifyResult(mojito, cocktail);

        cocktail = entityManager.createQuery("select c " +
          "from Cocktail c join Recipe r " +
          "on c.name = r.cocktail", Cocktail.class).getSingleResult();
        verifyResult(mojito, cocktail);

        // QueryDSL
        cocktail = new JPAQuery<Cocktail>(entityManager).from(QCocktail.cocktail)
          .join(QCocktail.cocktail.recipe)
          .fetchOne();
        verifyResult(mojito, cocktail);

        cocktail = new JPAQuery<Cocktail>(entityManager).from(QCocktail.cocktail)
          .join(QRecipe.recipe)
          .on(QCocktail.cocktail.name.eq(QRecipe.recipe.cocktail))
          .fetchOne();
        verifyResult(mojito, cocktail);
    }

    private void verifyResult(Cocktail expectedCocktail, Cocktail queryResult) {
        assertNotNull(queryResult);
        assertEquals(expectedCocktail, queryResult);
    }

    // ...
}

verifyResult メソッドは、クエリから返された結果が期待される結果と等しいことを確認するのに役立ちます。

7. 1対多の根底にある関係

例のドメインを変更して、1対多の基本的な関係を持つ2つのエンティティを結合する方法を示しましょう。

レシピテーブルの代わりに、 multiple_recipes テーブルがあり、同じカクテルに必要な数のレシピを格納できます。 。

@Entity
@Table(name = "multiple_recipes")
public class MultipleRecipe {
    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "cocktail")
    private String cocktail;

    @Column(name = "instructions")
    private String instructions;

    // getters & setters
}

現在、 Cocktailエンティティは、1対多の基本的な関係によってMultipleRecipeエンティティに関連付けられています

@Entity
@Table(name = "cocktails")
public class Cocktail {    
    // ...

    @OneToMany
    @NotFound(action = NotFoundAction.IGNORE)
    @JoinColumn(
       name = "cocktail", 
       referencedColumnName = "cocktail_name", 
       insertable = false, 
       updatable = false, 
       foreignKey = @javax.persistence
         .ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private List<MultipleRecipe> recipeList;

    // getters & setters
}

少なくとも1つの利用可能なMultipleRecipeがあるCocktailエンティティを検索して取得するために、関連すると結合することにより、Cocktailエンティティをクエリできます。 MultipleRecipeエンティティ。

これは、JPQLを使用して実行できます。

entityManager.createQuery("select c from Cocktail c join c.recipeList");

またはQueryDSLフレームワークを使用して:

new JPAQuery(entityManager).from(QCocktail.cocktail)
  .join(QCocktail.cocktail.recipeList);

使用しないオプションもありますレシピリスト間の1対多の関係を定義するフィールドカクテル MultipleRecipe エンティティ代わりに、2つのエンティティの結合クエリを記述し、JPQLの「on」句を使用してそれらの基本的な関係を判別できます。

entityManager.createQuery("select c "
  + "from Cocktail c join MultipleRecipe mr "
  + "on mr.cocktail = c.name");

最後に、QueryDSLフレームワークを使用して同じクエリを作成できます。

new JPAQuery(entityManager).from(QCocktail.cocktail)
  .join(QMultipleRecipe.multipleRecipe)
  .on(QCocktail.cocktail.name.eq(QMultipleRecipe.multipleRecipe.cocktail));

8. 1対多の結合ユニットテスト

ここでは、以前のクエリをテストするための新しいテストケースを追加します。 その前に、セットアップメソッド中にいくつかのMultipleRecipeインスタンスを永続化する必要があります。

public class UnrelatedEntitiesUnitTest {    
    // ...

    @BeforeAll
    public static void setup() {
        // ...
        
        MultipleRecipe firstMojitoRecipe = new MultipleRecipe();
        firstMojitoRecipe.setId(1L);
        firstMojitoRecipe.setCocktail(mojito.getName());
        firstMojitoRecipe.setInstructions("The first recipe of making a mojito!");
        entityManager.persist(firstMojitoRecipe);
        MultipleRecipe secondMojitoRecipe = new MultipleRecipe();
        secondMojitoRecipe.setId(2L);
        secondMojitoRecipe.setCocktail(mojito.getName());
        secondMojitoRecipe.setInstructions("The second recipe of making a mojito!"); 
        entityManager.persist(secondMojitoRecipe);
       
        // ...
    }

    // ... 
}

次に、テストケースを開発します。ここでは、前のセクションで示したクエリが実行されたときに、少なくとも1つのMultipleRecipeインスタンスに関連付けられているCocktailエンティティが返されることを確認します。 :

public class UnrelatedEntitiesUnitTest {
    // ...
    
    @Test
    public void givenCocktailsWithMultipleRecipes_whenQuerying_thenTheExpectedCocktailsReturned() {
        // JPQL
        Cocktail cocktail = entityManager.createQuery("select c "
          + "from Cocktail c join c.recipeList", Cocktail.class)
          .getSingleResult();
        verifyResult(mojito, cocktail);

        cocktail = entityManager.createQuery("select c "
          + "from Cocktail c join MultipleRecipe mr "
          + "on mr.cocktail = c.name", Cocktail.class)
          .getSingleResult();
        verifyResult(mojito, cocktail);

        // QueryDSL
        cocktail = new JPAQuery<Cocktail>(entityManager).from(QCocktail.cocktail)
          .join(QCocktail.cocktail.recipeList)
          .fetchOne();
        verifyResult(mojito, cocktail);

        cocktail = new JPAQuery<Cocktail>(entityManager).from(QCocktail.cocktail)
          .join(QMultipleRecipe.multipleRecipe)
          .on(QCocktail.cocktail.name.eq(QMultipleRecipe.multipleRecipe.cocktail))
          .fetchOne();
        verifyResult(mojito, cocktail);
    }

    // ...

}

9. 多対多の根底にある関係

このセクションでは、メニュー内のカクテルを基本的な材料で分類することを選択します。 たとえば、モヒートカクテルの基本成分はラム酒であるため、ラム酒はメニューのカクテルカテゴリです。

上記をドメインで表現するために、categoryフィールドをCocktailentityに追加します。

@Entity
@Table(name = "menu")
public class Cocktail {
    // ...

    @Column(name = "category")
    private String category;
    
     // ...
}

また、base_ingredient列をmultiple_recipesテーブルに追加して、特定の飲み物に基づいたレシピを検索できるようにすることもできます。

@Entity
@Table(name = "multiple_recipes")
public class MultipleRecipe {
    // ...
    
    @Column(name = "base_ingredient")
    private String baseIngredient;
    
    // ...
}

上記の後、データベーススキーマは次のようになります。

現在、カクテルエンティティとMultipleRecipeエンティティの間には多対多の基本的な関係があります。 多くのMultipleRecipeエンティティは、category値がMultipleRecipeのbaseIngredient値と等しい多くのCocktailエンティティに関連付けることができます。 エンティティ。

baseIngredientCocktailエンティティのカテゴリとして存在するMultipleRecipeエンティティを検索して取得するには、JPQLを使用して次の2つのエンティティを結合します。

entityManager.createQuery("select distinct r " 
  + "from MultipleRecipe r " 
  + "join Cocktail c " 
  + "on r.baseIngredient = c.category", MultipleRecipe.class)

またはQueryDSLを使用して:

QCocktail cocktail = QCocktail.cocktail; 
QMultipleRecipe multipleRecipe = QMultipleRecipe.multipleRecipe; 
new JPAQuery(entityManager).from(multipleRecipe)
  .join(cocktail)
  .on(multipleRecipe.baseIngredient.eq(cocktail.category))
  .fetch();

10. 多対多の参加ユニットテスト

テストケースを進める前に、CocktailエンティティのcategoryMultipleRecipeエンティティのbaseIngredientを設定する必要があります。

public class UnrelatedEntitiesUnitTest {
    // ...

    @BeforeAll
    public static void setup() {
        // ...

        mojito.setCategory("Rum");
        ginTonic.setCategory("Gin");
        firstMojitoRecipe.setBaseIngredient(mojito.getCategory());
        secondMojitoRecipe.setBaseIngredient(mojito.getCategory());

        // ...
    }

    // ... 
}

次に、前に示したクエリが実行されたときに、期待される結果が返されることを確認できます。

public class UnrelatedEntitiesUnitTest {
    // ...

    @Test
    public void givenMultipleRecipesWithCocktails_whenQuerying_thenTheExpectedMultipleRecipesReturned() {
        Consumer<List<MultipleRecipe>> verifyResult = recipes -> {
            assertEquals(2, recipes.size());
            recipes.forEach(r -> assertEquals(mojito.getName(), r.getCocktail()));
        };

        // JPQL
        List<MultipleRecipe> recipes = entityManager.createQuery("select distinct r "
          + "from MultipleRecipe r "
          + "join Cocktail c " 
          + "on r.baseIngredient = c.category",
          MultipleRecipe.class).getResultList();
        verifyResult.accept(recipes);

        // QueryDSL
        QCocktail cocktail = QCocktail.cocktail;
        QMultipleRecipe multipleRecipe = QMultipleRecipe.multipleRecipe;
        recipes = new JPAQuery<MultipleRecipe>(entityManager).from(multipleRecipe)
          .join(cocktail)
          .on(multipleRecipe.baseIngredient.eq(cocktail.category))
          .fetch();
        verifyResult.accept(recipes);
    }

    // ...
}

11. 結論

このチュートリアルでは、関連のないエンティティ間で、JPQLまたはQueryDSLフレームワークを使用してJPAクエリを構築するさまざまな方法を紹介しました。

いつものように、コードはGitHubから入手できます。