В предыдущей статье мы узнали, как добавить полнотекстовый поиск в Spring Boot Rest API с помощью Hibernate Search.
В этой статье мы будем развивать эту идею и узнаем, как добавить постраничный поиск в существующий REST API.
Настройка проекта
Вы можете ознакомиться с предыдущей статьей блога, чтобы получить подробное описание того, как настроить проект с помощью Spring Initializer.
Вы также можете получить конечный результат последней статьи на Github.
Расширение модели данных
Первое, что необходимо сделать, это найти способ получения новых данных, необходимых для добавления пагинации.
Для этого мы можем расширить SearchRequestDTO.
package com.mozen.springbootpaginatedsearch.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Min;
@Data
@EqualsAndHashCode(callSuper = true)
public class PageableSearchRequestDTO extends SearchRequestDTO{
@Min(0)
private int pageOffset;
}
Нам нужно определить только одно новое поле – pageOffset. Это поле используется для управления индексом страницы, которую мы хотим запросить.
Мы также определяем новую структуру данных PageDTO. Эта структура данных используется для хранения результатов нашего постраничного поиска.
package com.mozen.springbootpaginatedsearch.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageDTO<T> {
private List<T> content;
private long total;
}
Расширение слоя данных
Мы объявляем новую функцию searchPageBy в интерфейсе SearchRepository.
package com.mozen.springbootpaginatedsearch.repository;
import com.mozen.springbootpaginatedsearch.model.PageDTO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
import java.io.Serializable;
import java.util.List;
@NoRepositoryBean
public interface SearchRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
List<T> searchBy(String text, int limit, String... fields);
PageDTO<T> searchPageBy(String text, int limit, int offset, String... fields);
}
Сигнатура очень похожа на существующую функцию searchBy. Мы просто добавляем новый параметр смещения, который указывает на страницу для запроса.
Мы копируем это изменение в класс SearchRepositoryImpl.
package com.mozen.springbootpaginatedsearch.repository;
import com.mozen.springbootpaginatedsearch.model.PageDTO;
import org.hibernate.search.engine.search.query.SearchResult;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.List;
@Transactional
public class SearchRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
implements SearchRepository<T, ID> {
private final EntityManager entityManager;
public SearchRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
public SearchRepositoryImpl(
JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
public List<T> searchBy(String text, int limit, String... fields) {
SearchResult<T> result = getSearchResult(text, limit, 0, fields);
return result.hits();
}
@Override
public PageDTO<T> searchPageBy(String text, int limit, int offset, String... fields) {
SearchResult<T> result = getSearchResult(text, limit, offset, fields);
return new PageDTO<T>(result.hits(), result.total().hitCount());
}
private SearchResult<T> getSearchResult(String text, int limit, int offset, String[] fields) {
SearchSession searchSession = Search.session(entityManager);
SearchResult<T> result =
searchSession
.search(getDomainClass())
.where(f -> f.match().fields(fields).matching(text).fuzzy(2))
.fetch(offset, limit);
return result;
}
}
Мы можем повторно использовать существующий метод getSearchResult, добавив новый аргумент “offset”. Затем мы используем этот аргумент в методе Hibernate Search fetch(), который уже содержит сигнатуру, принимающую параметр смещения для целей пагинации.
PageDTO создается с использованием результата поискового запроса.
Расширение бизнес-слоя
Мы можем опираться на существующую логику, извлекая часть, обрабатывающую поле для поиска, чтобы избежать дублирования, а затем вызвать функцию хранилища с пагинацией или без нее в зависимости от того, используем ли мы метод searchPlant() или метод searchPlantPage().
package com.mozen.springbootpaginatedsearch.service;
import com.mozen.springbootpaginatedsearch.model.PageDTO;
import com.mozen.springbootpaginatedsearch.model.Plant;
import com.mozen.springbootpaginatedsearch.repository.PlantRepository;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class PlantService {
private PlantRepository plantRepository;
private static final List<String> SEARCHABLE_FIELDS = Arrays.asList("name","scientificName","family");
public PlantService(PlantRepository plantRepository) {
this.plantRepository = plantRepository;
}
public List<Plant> searchPlants(String text, List<String> fields, int limit) {
List<String> fieldsToSearchBy = getFieldsToSearchBy(fields);
return plantRepository.searchBy(
text, limit, fieldsToSearchBy.toArray(new String[0]));
}
public PageDTO<Plant> searchPlantPage(String text, List<String> fields, int limit, int pageOffset) {
List<String> fieldsToSearchBy = getFieldsToSearchBy(fields);
return plantRepository.searchPageBy(
text, limit, pageOffset, fieldsToSearchBy.toArray(new String[0]));
}
// We extract the common logic in a separate function
private List<String> getFieldsToSearchBy(List<String> fields) {
List<String> fieldsToSearchBy = fields.isEmpty() ? SEARCHABLE_FIELDS : fields;
boolean containsInvalidField = fieldsToSearchBy.stream(). anyMatch(f -> !SEARCHABLE_FIELDS.contains(f));
if(containsInvalidField) {
throw new IllegalArgumentException();
}
return fieldsToSearchBy;
}
}
Расширение веб-слоя
Здесь не так много нужно сделать.
Нам просто нужна новая конечная точка для получения постраничных поисковых запросов с помощью нашего нового PageableSearchRequestDTO и возврата PageDTO.
package com.mozen.springbootpaginatedsearch.controller;
import com.mozen.springbootpaginatedsearch.model.PageDTO;
import com.mozen.springbootpaginatedsearch.model.PageableSearchRequestDTO;
import com.mozen.springbootpaginatedsearch.model.Plant;
import com.mozen.springbootpaginatedsearch.model.SearchRequestDTO;
import com.mozen.springbootpaginatedsearch.service.PlantService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/plant")
public class PlantController {
private PlantService plantService;
public PlantController(PlantService plantService) {
this.plantService = plantService;
}
@GetMapping("/search")
public List<Plant> searchPlants(SearchRequestDTO searchRequestDTO) {
log.info("Request for plant search received with data : " + searchRequestDTO);
return plantService.searchPlants(searchRequestDTO.getText(), searchRequestDTO.getFields(), searchRequestDTO.getLimit());
}
@GetMapping("/search/page")
public PageDTO<Plant> searchPlantPage(PageableSearchRequestDTO pageableSearchRequestDTO) {
log.info("Request for plant page search received with data : " + pageableSearchRequestDTO);
return plantService.searchPlantPage(pageableSearchRequestDTO.getText(), pageableSearchRequestDTO.getFields(), pageableSearchRequestDTO.getLimit(), pageableSearchRequestDTO.getPageOffset());
}
}
Мы регистрируем полученные данные запроса и вызываем нашу новую функцию, определенную в plantService.
Собираем все вместе
Пора протестировать наш код!
Мы можем запустить наше приложение с помощью командной строки.
mvn spring-boot:run
Как и в первой статье, мы можем использовать Postman ….
Или мы можем использовать простую команду cUrl.
// Request page 1 with 2 items per page on all fields
curl -X GET 'http://localhost:9000/plant/search?text=cherry&limit=2&pageOffset=1'
// Request page 2 with 3 items per page on scientificName field
curl -X GET 'http://localhost:9000/plant/search?text=asian&limit=3&fields=name&fields=scientificName&pageOffset=2'
И мы закончили! Наша реализация полнотекстового поиска теперь поддерживает пагинацию.
Мы еще многое можем добавить к нашей реализации, и я сделаю это в следующих статьях.
Вы можете получить доступ к демонстрационному проекту для этой статьи блога здесь https://github.com/Mozenn/spring-boot-paginated-search.