Często jestem proszony o opracowanie zaawansowanych usług wyszukiwania. Przez zaawansowane wyszukiwanie rozumiem wyszukiwanie, w którym możliwe jest zastosowanie wielu filtrów do wszystkich (lub prawie wszystkich) pól, takich jak like, between, in, greater than itp.
Proszę więc wyobrazić sobie konieczność zbudowania usługi opartej na jednej lub kilku jednostkach zdolnych do oferowania punktu końcowego, który można nazwać w ten sposób (proszę zacząć zwracać uwagę na specjalne sufiksy <propertyName><_suffix>):
curl - request GET \
- url 'http://www.myexampledomain.com/persons?
firstName=Biagio
&lastName_startsWith=Toz
&birthDate_gte=19910101
&country_in=IT,FR,DE
&company.name_in=Microsoft,Apple
&company.employees_between=500,5000'
Lub
curl --request GET \
--url 'http://www.myexampledomain.com/persons?
firstName_endsWith=gio
&lastName_in=Tozzi,Totti
&birthDate_lt=19980101
&_offset=0
&_limit=100
&birthDate_sort=ASC'
Jeśli używają Państwo JPA w projekcie Spring Boot, mogą Państwo teraz opracować tę usługę wyszukiwania za pomocą zaledwie kilku wierszy kodu, dzięki aplikacji JPA Search Helper! Proszę pozwolić mi wyjaśnić, co to jest.
JPA Search Helper
Pierwszy krok: Adnotacja @Searchable
Proszę zacząć od zastosowania @Searchable adnotacji do pól w Państwa DTO lub alternatywnie w Państwa encji JPA, które mają być dostępne do wyszukiwania.
@Data
public class Person {
@Searchable
private String firstName;
@Searchable
private String lastName;
@Searchable(entityFieldKey = "dateOfBirth")
private Date birthDate;
@Searchable
private String country;
private Company company;
@Data
public static class Company {
@Searchable(entityFieldKey=companyEntity.name)
private String name;
@Searchable(entityFieldKey=companyEntity.employeesCount)
private int employees;
}
}
Adnotacja umożliwia określenie:
- Podstawowe właściwości
- entityFieldKey: Nazwa pola zdefiniowanego w encji bean (nie należy podawać, jeśli używana jest adnotacja w encji bean). Jeśli nie określono, kluczem będzie nazwa pola.
- targetType: Typ obiektu zarządzanego przez podmiot. Jeśli nie zostanie określony, biblioteka spróbuje uzyskać go na podstawie typu pola (np. Integer pole bez definicji typu docelowego będzie INTEGER). Jeśli nie ma typu zgodnego z tymi zarządzanymi, będzie on zarządzany jako ciąg znaków. Typy zarządzane: STRING, INTEGER, DOUBLE, FLOAT, LONG, BIGDECIMAL, BOOLEAN, DATE, LOCALDATE, LOCALDATETIME, LOCALTIME, OFFSETDATETIME, OFFSETTIME.
- Właściwości walidacji
- datePattern: Tylko dla DATA typ docelowy. Określa wzorzec daty do użycia.
- maxSize, minSize: Maksymalna/minimalna długość wartości.
- maxDigits, minDigits: Tylko dla typów numerycznych. Maksymalna/minimalna liczba cyfr.
- regexPattern: Regex pattern.
- decimalFormat: Tylko dla dziesiętnych typów liczbowych. Domyślnie #.##.
Kontynuując przykład, nasze klasy encji:
@Entity
@Data
public class PersonEntity {
@Id
private Long id;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Column(name = "BIRTH_DATE")
private Date dateOfBirth;
@Column(name = "COUNTRY")
private String country;
@OneToOne
private CompanyEntity companyEntity;
}
@Entity
@Data
public class CompanyEntity {
@Id
private Long id;
@Column(name = "NAME")
private String name;
@Column(name = "COUNT")
private Integer employeesCount;
}
Drugi i ostatni krok: JPASearchRepository<?>
Państwa repozytorium Spring JPA musi rozszerzać JPASearchRepository<?>:
@Repository
public interface PersonRepository extends JpaRepository<PersonEntity, Long>, JPASearchRepository<PersonEntity> {
}
Cóż, zbudujmy filtry i prześlijmy je do repozytorium:
// ...
Map<String, String> filters = new HashMap<>();
filters.put("firstName_eq", "Biagio");
filters.put("lastName_startsWith", "Toz");
filters.put("birthDate_gte", "19910101");
filters.put("country_in", "IT,FR,DE");
filters.put("company.name_in", "Microsoft,Apple");
filters.put("company.employees_between", "500,5000");
// Without pagination
List<PersonEntity> fullSearch = personRepository.findAll(filters, Person.class);
filters.put("birthDate_sort" : "ASC");
filters.put("_limit", "10");
filters.put("_offset", "0");
// With pagination
Page<PersonEntity> sortedAndPaginatedSearch = personRepository.findAllWithPaginationAndSorting(filters, Person.class);
// ...
Zasadniczo wystarczy zdefiniować mapę, której klucz składa się z <fieldName><_suffix> i wartość wyszukiwania. Pełna lista sufiksów, tj. dostępnych filtrów, to tutaj.
Uwaga 1: Jeśli nie określono przyrostka, wyszukiwanie odbywa się w równych (_eq)
Uwaga 2: W przykładzie zastosowałem adnotację @Searchable do pól DTO. Alternatywnie można zastosować je bezpośrednio na encji.
Pseudo-rzeczywista implementacja w projekcie Spring Boot
Service/Manager bean:
@Service
public class PersonManager {
@Autowired
private PersonRepository personRepository;
public List<Person> find(Map<String, String> filters) {
return personRepository.findAllWithPaginationAndSorting(filters, Person.class).stream().map(this::toDTO).toList();
}
private static Person toDTO(PersonEntity personEntity) {
// ...
}
}
Kontroler:
@RestController
public class MyController {
@Autowired
private PersonManager personManager;
@GetMapping(path="/persons", produces = MediaType.APPLICATION_JSON_VALUE)
public List<Person> findPersons(@RequestParam Map<String, String> requestParams) {
return personManager.find(requestParams);
}
}
..et voilà les jeux sont faits
Extra
Biblioteka umożliwia wymuszenie dołączenia do pobierania.
A “fetch” join umożliwia inicjalizację asocjacji lub kolekcji wartości wraz z ich obiektami nadrzędnymi za pomocą pojedynczego wyboru.
W ten sposób:
// ...
Map<String, JoinFetch> fetches = Map.of("companyEntity", JoinFetch.LEFT);
personRepository.findAll(filters, Person.class, fetches);
// ...
To wszystko… na razie!