1. Введение
Jinq предоставляет интуитивно понятный и удобный подход для запросов к базам данных на Java. В этом руководстве мы рассмотрим, как настроить проект Spring для использования Jinq и некоторых его функций, проиллюстрированных на простых примерах.
2. Зависимости Maven
Нам нужно добавить зависимость Jinq
в файл pom.xml:
<dependency>
<groupId>org.jinq</groupId>
<artifactId>jinq-jpa</artifactId>
<version>1.8.22</version>
</dependency>
Для Spring мы добавим зависимость Spring ORM в файл pom.xml
:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.3</version>
</dependency>
Наконец, для тестирования мы будем использовать базу данных H2 в памяти, поэтому давайте также добавим эту зависимость вместе с spring-boot-starter-data-jpa
в файл pom.xml:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.6.1</version>
</dependency>
3. Понимание Джинка
Jinq помогает нам писать более простые и читаемые запросы к базе данных, предоставляя гибкий API, который внутренне основан на Java Stream API.
Давайте посмотрим на пример, где мы фильтруем автомобили по модели:
jinqDataProvider.streamAll(entityManager, Car.class)
.where(c -> c.getModel().equals(model))
.toList();
Jinq эффективно переводит приведенный выше фрагмент кода в SQL-запрос , поэтому окончательный запрос в этом примере будет таким:
select c.* from car c where c.model=?
Поскольку мы не используем обычный текст для написания запросов, а вместо этого используем типобезопасный API, этот подход менее подвержен ошибкам.
Кроме того, Jinq стремится обеспечить более быструю разработку за счет использования общих, легко читаемых выражений.
Тем не менее, у него есть некоторые ограничения на количество типов и операций, которые мы можем использовать, как мы увидим далее.
3.1. Ограничения
Jinq поддерживает только основные типы в JPA и конкретный список функций SQL. Он работает путем преобразования лямбда-операций в собственный SQL-запрос путем сопоставления всех объектов и методов с типом данных JPA и функцией SQL.
Поэтому мы не можем ожидать, что инструмент будет переводить каждый пользовательский тип или все методы типа.
3.2. Поддерживаемые типы данных
Давайте посмотрим поддерживаемые типы данных и поддерживаемые методы:
Строка
– только методыequals()
,compareTo()
- Примитивные типы данных — арифметические операции
Перечисления
и пользовательские классы — поддерживаются только операции == и !=java.util.Collection –
содержит()
Date
API — только методыequals()
,before()
,after()
Примечание: если мы хотим настроить преобразование из объекта Java в объект базы данных, нам нужно будет зарегистрировать нашу конкретную реализацию AttributeConverter
в Jinq.
4. Интеграция Jinq со Spring
Jinq нужен экземпляр EntityManager
для получения контекста персистентности. В этом уроке мы представим простой подход с помощью Spring, чтобы заставить Jinq работать с EntityManager
, предоставляемым Hibernate .
4.1. Интерфейс репозитория
Spring использует концепцию репозиториев для управления сущностями. Давайте посмотрим на наш интерфейс CarRepository
, где у нас есть метод для получения Car
для данной модели:
public interface CarRepository {
Optional<Car> findByModel(String model);
}
4.2. Абстрактный базовый репозиторий
Далее нам понадобится базовый репозиторий для предоставления всех возможностей Jinq:
public abstract class BaseJinqRepositoryImpl<T> {
@Autowired
private JinqJPAStreamProvider jinqDataProvider;
@PersistenceContext
private EntityManager entityManager;
protected abstract Class<T> entityType();
public JPAJinqStream<T> stream() {
return streamOf(entityType());
}
protected <U> JPAJinqStream<U> streamOf(Class<U> clazz) {
return jinqDataProvider.streamAll(entityManager, clazz);
}
}
4.3. Реализация репозитория
Теперь все, что нам нужно для Jinq, — это экземпляр EntityManager
и класс типа сущности.
Давайте посмотрим на реализацию репозитория Car
, используя наш базовый репозиторий Jinq, который мы только что определили:
@Repository
public class CarRepositoryImpl
extends BaseJinqRepositoryImpl<Car> implements CarRepository {
@Override
public Optional<Car> findByModel(String model) {
return stream()
.where(c -> c.getModel().equals(model))
.findFirst();
}
@Override
protected Class<Car> entityType() {
return Car.class;
}
}
4.4. Подключение JinqJPAStreamProvider
Чтобы связать экземпляр JinqJPAStreamProvider
, мы добавим конфигурацию поставщика Jinq:
@Configuration
public class JinqProviderConfiguration {
@Bean
@Autowired
JinqJPAStreamProvider jinqProvider(EntityManagerFactory emf) {
return new JinqJPAStreamProvider(emf);
}
}
4.5. Настройка приложения Spring
Последним шагом является настройка нашего приложения Spring с использованием Hibernate и нашей конфигурации Jinq. В качестве справки см. наш файл application.properties
, в котором мы используем экземпляр H2 в памяти в качестве базы данных:
spring.datasource.url=jdbc:h2:~/jinq
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
5. Руководство по запросам
Jinq предоставляет множество интуитивно понятных опций для настройки окончательного SQL-запроса с помощью select, where,
joins и т. д.
Обратите внимание, что они имеют те же ограничения, которые мы уже ввели выше.
5.1. Где
Предложение where
позволяет применять несколько фильтров к набору данных.
В следующем примере мы хотим отфильтровать автомобили по модели и описанию:
stream()
.where(c -> c.getModel().equals(model)
&& c.getDescription().contains(desc))
.toList();
А это SQL, который переводит Jinq:
select c.model, c.description from car c where c.model=? and locate(?, c.description)>0
5.2. Выбирать
Если мы хотим получить только несколько столбцов/полей из базы данных, нам нужно использовать предложение select
.
Для сопоставления нескольких значений Jinq предоставляет несколько классов Tuple
, содержащих до восьми значений:
stream()
.select(c -> new Tuple3<>(c.getModel(), c.getYear(), c.getEngine()))
.toList()
И переведенный SQL:
select c.model, c.year, c.engine from car c
5.3. Присоединяется
Jinq может разрешать отношения «один к одному» и «многие к одному» , если сущности правильно связаны.
Например, если мы добавим объект производителя в Car
:
@Entity(name = "CAR")
public class Car {
//...
@OneToOne
@JoinColumn(name = "name")
public Manufacturer getManufacturer() {
return manufacturer;
}
}
И объект Manufacturer со списком
Car
s:
@Entity(name = "MANUFACTURER")
public class Manufacturer {
// ...
@OneToMany(mappedBy = "model")
public List<Car> getCars() {
return cars;
}
}
Теперь мы можем получить производителя
для данной модели:
Optional<Manufacturer> manufacturer = stream()
.where(c -> c.getModel().equals(model))
.select(c -> c.getManufacturer())
.findFirst();
Как и ожидалось, в этом сценарии Jinq будет использовать SQL-предложение внутреннего соединения :
select m.name, m.city from car c inner join manufacturer m on c.name=m.name where c.model=?
В случае, если нам нужно иметь больший контроль над предложениями соединения
, чтобы реализовать более сложные отношения над сущностями, такие как отношение «многие ко многим», мы можем использовать метод соединения
:
List<Pair<Manufacturer, Car>> list = streamOf(Manufacturer.class)
.join(m -> JinqStream.from(m.getCars()))
.toList()
Наконец, мы могли бы использовать SQL-предложение левого внешнего соединения, используя метод leftOuterJoin
вместо метода соединения
.
5.4. Агрегации
Все примеры, которые мы представили до сих пор, используют методы toList
или findFirst
для возврата окончательного результата нашего запроса в Jinq.
Помимо этих методов, у нас также есть доступ к другим методам агрегирования результатов .
Например, давайте воспользуемся методом count
, чтобы получить общее количество автомобилей для конкретной модели в нашей базе данных:
long total = stream()
.where(c -> c.getModel().equals(model))
.count()
И окончательный SQL использует метод count
SQL, как и ожидалось:
select count(c.model) from car c where c.model=?
Jinq также предоставляет методы агрегирования, такие как сумма
, среднее
, минимальное
, максимальное значение,
а также возможность комбинировать различные агрегации .
5.5. Пагинация
Если мы хотим читать данные пакетами, мы можем использовать методы limit
и skip .
Давайте посмотрим на пример, где мы хотим пропустить первые 10 автомобилей и получить только 20 предметов:
stream()
.skip(10)
.limit(20)
.toList()
И сгенерированный SQL:
select c.* from car c limit ? offset ?
6. Заключение
Ну вот. В этой статье мы рассмотрели подход к настройке приложения Spring с Jinq с использованием Hibernate (минимально).
Мы также кратко рассмотрели преимущества Jinq и некоторые из его основных функций.
Как всегда, исходники можно найти на GitHub .