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

Введение в Jinq со Spring

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

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 .