«1. Обзор

В этом руководстве мы увидим, как мы можем построить запрос JPA между несвязанными объектами.

2. Зависимости Maven

Давайте начнем с добавления необходимых зависимостей в наш pom.xml.

Прежде всего, нам нужно добавить зависимость для Java Persistence API:

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

Затем мы добавляем зависимость для Hibernate ORM, которая реализует Java Persistence API:

<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. Модель предметной области

Областью нашего примера является коктейль-бар. Здесь у нас есть две таблицы в базе данных:

    Таблица меню для хранения коктейлей, которые продает наш бар, и их цены, и Таблица рецептов для хранения инструкций по созданию коктейля

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

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

4. Сущности JPA

Мы можем легко создать две сущности 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
}

Чтобы представить эту связь в нашей сущности Cocktail, мы добавляем поле рецепта, аннотированное различными аннотациями:

Первая аннотация — @OneToOne, которая объявляет лежащую в основе отношение один к одному с сущностью Recipe.

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

Далее мы аннотируем поле рецепта аннотацией @NotFound(action = NotFoundAction.IGNORE) Hibernate. Это говорит нашему ORM не генерировать исключение, когда есть рецепт коктейля, которого нет в нашей таблице меню.

Аннотация, которая связывает Коктейль со связанным с ним Рецептом, — @JoinColumn. Используя эту аннотацию, мы определяем отношение псевдовнешнего ключа между двумя объектами.

Наконец, установив для свойства foreignKey значение @javax.persistence.ForeignKey(value = ConstraintMode.NO_CONSTRAINT), мы указываем поставщику JPA не генерировать ограничение внешнего ключа.

5. Запросы JPA и QueryDSL

Поскольку нас интересует получение объектов Cocktail, связанных с Recipe, мы можем запросить объект Cocktail, объединив его с ассоциированным объектом Recipe.

Одним из способов создания запроса является использование JPQL:

Или использование инфраструктуры QueryDSL:

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

Другой способ получить желаемые результаты — соединить Cocktail с сущностью Recipe. и с помощью предложения on для прямого определения базовой связи в запросе.

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

Мы можем сделать это с помощью JPQL:

или с помощью фреймворка QueryDSL:

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

6. Модульный тест соединения \»один к одному\»

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

Давайте начнем создавать модульный тест для тестирования. вышеуказанные запросы. Перед запуском наших тестовых случаев мы должны вставить некоторые данные в таблицы нашей базы данных.

В методе настройки мы сохраняем две сущности Cocktail, mojito и ginTonic. Затем мы добавляем рецепт приготовления коктейля «Мохито».

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);
      
        // ...
    }

    // ... 
}

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

7. Базовая связь «один ко многим»

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);
    }

    // ...
}

Давайте изменим домен нашего примера, чтобы показать, как мы можем соединить две сущности с базовой связью «один ко многим».

Вместо таблицы recipes у нас есть таблица multiple_recipes, в которой мы можем хранить сколько угодно рецептов для одного и того же коктейля.

«

«Теперь сущность Cocktail связана с сущностью MultipleRecipe базовым отношением «один ко многим»:

@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, для которых у нас есть хотя бы один доступный MultipleRecipe, мы можем запросить сущность Cocktail. присоединив его к связанным объектам 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
}

Мы можем сделать это, используя JPQL:

или используя инфраструктуру QueryDSL:

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

Существует также возможность не использовать поле recipeList, которое определяет отношение «один ко многим» между сущности Cocktail и MultipleRecipe. Вместо этого мы можем написать запрос соединения для двух сущностей и определить их базовую связь с помощью предложения JPQL on:

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

Наконец, мы можем построить тот же запрос, используя структуру QueryDSL:

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

~ ~~ 8. Модульный тест соединения «один ко многим»

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

Здесь мы добавим новый тестовый пример для тестирования предыдущих запросов. Прежде чем сделать это, мы должны сохранить несколько экземпляров MultipleRecipe во время нашего метода настройки:

Затем мы можем разработать тестовый пример, в котором мы проверяем, что при выполнении запросов, которые мы показали в предыдущем разделе, они возвращают Cocktail сущности, связанные хотя бы с одним экземпляром 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);
       
        // ...
    }

    // ... 
}

9. Базовая связь «многие ко многим»

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);
    }

    // ...

}

В этом разделе мы выбираем классификацию наших коктейлей в нашем меню по их основному ингредиенту. Например, базовым ингредиентом коктейля мохито является ром, поэтому ром является категорией коктейлей в нашем меню.

Чтобы отобразить вышеописанное в нашем домене, мы добавим поле категории в сущность Cocktail:

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

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

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

После вышеизложенного вот схема нашей базы данных:

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

Теперь у нас есть базовая связь «многие ко многим» между сущностями Cocktail и MultipleRecipe. Многие сущности MultipleRecipe могут быть связаны со многими сущностями Cocktail, если их значение категории равно значению baseIngredient сущностей MultipleRecipe.

Чтобы найти и получить объекты MultipleRecipe, для которых их baseIngredient существует как категория в объектах Cocktail, мы можем соединить эти два объекта с помощью JPQL:

Или с помощью QueryDSL:

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

10 Модульный тест соединения «многие ко многим»

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

Прежде чем приступить к нашему тестовому примеру, мы должны установить категорию наших сущностей Cocktail и baseIngredient наших сущностей MultipleRecipe:

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

public class UnrelatedEntitiesUnitTest {
    // ...

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

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

        // ...
    }

    // ... 
}

11. Заключение

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);
    }

    // ...
}

В этом руководстве мы представили различные способы построения запросов JPA между несвязанными объектами и с использованием JPQL или фреймворка QueryDSL.

Как всегда, код доступен на GitHub.

«