Перейти к основному содержимому

Создание запроса JPA между несвязанными объектами

· 9 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

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

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

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

./f66f1e98892dd77c65ddca4aa223c452.png

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

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

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 , мы добавляем поле рецепта с различными аннотациями:

@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 , которая объявляет базовое отношение «один к одному» с сущностью « Рецепт ».

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

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

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

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

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

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

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

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

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

Другой способ получить желаемые результаты — соединить 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);

// ...
}

// ...
}

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

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

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. Базовая связь «один ко многим»

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

./ed942c33b36dc1dc7c102ad140c31edc.png

Вместо таблицы recipes у нас есть таблица 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 связана с сущностью 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
}

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

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

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

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

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

Существует также возможность не использовать поле recipeList , которое определяет отношение «один ко многим» между сущностями Cocktail и 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. Модульный тест соединения «один ко многим»

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

// ...
}

// ...
}

Затем мы можем разработать тестовый пример, в котором мы проверяем, что при выполнении запросов, которые мы показали в предыдущем разделе, они возвращают сущности 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<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. Базовая связь «многие ко многим»

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

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

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

// ...
}

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

./a23fe2cf529ea42db4ad4b286f2032e8.png

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

Чтобы найти и получить сущности MultipleRecipe , в которых их baseIngredient существует как категория в сущностях Cocktail , мы можем соединить эти две сущности с помощью 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. Модульный тест соединения «многие ко многим»

Прежде чем продолжить наш тестовый пример, мы должны установить категорию наших сущностей 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());

// ...
}

// ...
}

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

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. Заключение

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

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