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