Рубрики
Без рубрики

Построение запроса JPA Между Несвязанными Сущностями

Научитесь создавать запрос JPA между несвязанными сущностями.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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


   javax.persistence
   javax.persistence-api
   2.2

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


    org.hibernate
    hibernate-core
    5.4.14.Final

И, наконец, мы добавляем некоторые зависимости QueryDSL , а именно querydsl-apt и querydsl-jpa :


    com.querydsl
    querydsl-apt
    4.3.1
    provided


    com.querydsl
    querydsl-jpa
    4.3.1

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
}

Между таблицами menu и recipes существует базовая связь “один к одному” без явного ограничения внешнего ключа . Например, если у нас есть запись menu , в которой значение столбца cocktail_name равно “Мохито”, и запись recipes , в которой значение столбца cocktail равно “Мохито”, то запись menu связана с этой записью recipes .

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

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

Затем мы аннотируем поле рецепт с помощью @NotFound(действие.ИГНОРИРОВАТЬ) Аннотацию Hibernate. Это говорит нашей ФОРМЕ не выдавать исключение, когда есть рецепт для коктейля , которого нет в нашей таблице меню .

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

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

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

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

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

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

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

new JPAQuery(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. Модульный Тест Соединения Один К Одному

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

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

    // ... 
}

В методе setup мы сохраняем две сущности Cocktail , mojito и джин-тоник. Затем мы добавляем рецепт того, как мы можем сделать “Мохито” коктейль .

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

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(entityManager).from(QCocktail.cocktail)
          .join(QCocktail.cocktail.recipe)
          .fetchOne();
        verifyResult(mojito, cocktail);

        cocktail = new JPAQuery(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);
    }

    // ...
}

Метод verify Result помогает нам убедиться, что результат, возвращенный из запроса, равен ожидаемому результату.

7. Отношение “Один Ко Многим”

Давайте изменим область нашего примера, чтобы показать, как мы можем объединить две сущности с базовым отношением “один ко многим” .

Вместо того, чтобы рецепты стол, у нас есть 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
}

Теперь сущность Коктейль связана с сущностью Множественный рецепт с помощью отношения “один ко многим” :

@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 recipeList;

    // getters & setters
}

Чтобы найти и получить сущности 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);

Существует также возможность не использовать поле Список рецептов , которое определяет отношение “один ко многим” между сущностями Коктейль и MultipleRecipe . Вместо этого мы можем написать запрос на соединение для двух сущностей и определить их базовую связь с помощью предложения 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. Модульный Тест “Один Ко Многим”

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

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

    // ... 
}

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

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(entityManager).from(QCocktail.cocktail)
          .join(QCocktail.cocktail.recipeList)
          .fetchOne();
        verifyResult(mojito, cocktail);

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

    // ...

}

9. Лежащие В Основе Отношения “Многие Ко Многим”

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

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

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

После вышесказанного, вот ваша схема базы данных:

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

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

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. Многие Ко Многим Присоединяются К Модульному Тесту

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

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> verifyResult = recipes -> {
            assertEquals(2, recipes.size());
            recipes.forEach(r -> assertEquals(mojito.getName(), r.getCocktail()));
        };

        // JPQL
        List 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(entityManager).from(multipleRecipe)
          .join(cocktail)
          .on(multipleRecipe.baseIngredient.eq(cocktail.category))
          .fetch();
        verifyResult.accept(recipes);
    }

    // ...
}

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

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

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