Multiple dynamic parameters to search with Spring Boot, sometimes someparam can be empty - spring

I am working on Angular 2 with a Spring Boot application, and I am passing some search parameters through my UI to SpringBoot RestController where I am using the CrudRepository<User, id> interface for CRUD operations and am using some methods like findByValue(), but in my app I have four search boxes, like
name, age, city, and country.
If I fill only two boxes then I would like to search for that given two parameters like Age=22, City=New York then only those people who lives in New York with age 22 should be as result if I add name also like name=James then it should search with name and age 22 and city New York.
How do I achieve this kind of functionality in my SpringBoot with a Angular 2 app?
Repository Class:
public interface UserRepository extends CrudRepository<User, Integer> {
public List<User> findAll();
public List<User> findByName(String name);
}
My Controller Code:
#CrossOrigin(origins = "http://localhost:4200", maxAge = 3600)
#RequestMapping(value = "/user/list", method=RequestMethod.POST)
public List<User> getRequestedUsers(#RequestBody User userObject) {
return userRepository.findByAllParam();
// I want to write some method here to get data with all or may be 3, 2, or 1 parameters
}

If your controller is simply returning the repository response, you're better off using Spring Data REST:
https://spring.io/guides/gs/accessing-data-rest/
Once setup, you can expose your repository endpoints directly over REST:
#RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
List<User> findByName(#Param("name") String name);
List<User> findByNameAndAge(#Param("name") String name, #Param("age") Integer age)
List<User> findByNameAndAgeAndCity(#Param("name") String name, #Param("age") Integer age, #Param("city") String city);
}
This then takes care of paging, sorting etc also.
You will end up with an API similar to:
curl -i -X POST -H "Content-Type:application/json" -d "{ \"name\" : \"...\", \"age\" : 0, \"city\" : \"...\" }" http://localhost: 4200/users
In terms of searching with optional parameters, you could manage this on the client side in JavaScript. The following solution doesn't scale but should help you get something working:
function hasValue(id) {
return document.getElementById("age").value !== ''
}
function doSearch() {
const nameHasValue = hasValue("name");
const ageHasValue = hasValue("age");
const cityHasValue = hasValue("city");
if (nameHasValue && ageHasValue && cityHasValue) {
// search by name, age and city
} else if (nameHasValue && ageHasValue) {
// search by name and age
} else if (nameHasValue && cityHasValue) {
// search by name and city
} else if (ageHasValue && cityHasValue) {
// search by age and city
} else if (nameHasValue) {
// search by name
} else if (ageHasValue) {
// search by age
} else if (cityHasValue) {
// search by city
}
}

public interface OrdersRepository extends CrudRepository<OrdersModel, Integer> {
List<OrdersModel> findAllByCityLikeAndPartLike(String city, String part);
}
And if you want searching by city = New York and part = any String:
findAllByCityLikeAndPartLike("New York", "%");
Not exactly problem like yours but I think that will be helpful.

Related

Handling multiple possible #RequestParam values when making request

I have an endpoint to get all Posts, I also have multiple #RequestParams used to filter and search for values etc.
The issue I'm having is that when trying to filter based on specific #RequestParams, I would need to have multiple checks to see whether that specific parameter is passed when calling the endpoint, so in my Controller I have something like this. The parameters are optional, I also have parameters for Pagination etc, but I left it out below.
I have these criteria:
#RequestParam(required=false) List<String> brand - Used to filter by multiple brands
#RequestParam(required=false) String province - Used to filter by province
#RequestParam(required=false) String city - Used to filter by city
// Using these 2 for getting Posts within a certain price range
#RequestParam(defaultValue = "0", required = false) String minValue - Used to filter by min price
#RequestParam(defaultValue = "5000000", required = false) String maxValue - Used to filter by max price
I also have this in my Controller when checking which of my service methods to call based on the parameters passed.
if(query != null) {
pageTuts = postService.findAllPosts(query, pagingSort);
} else if(brand != null) {
pageTuts = postService.findAllByBrandIn(brand, pagingSort);
} else if(minValue != null && maxValue != null) {
pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
} else if(brand != null & minValue != null & maxValue != null) {
pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
} else {
// if no parameters are passed in req, just get all the Posts available
pageTuts = postService.findAllPosts(pagingSort);
}
// I would need more checks to handle all parameters
The issue is that I'm struggling to find out, if I need this condition for each and every possible parameter, which will be a lot of checks and Repository/Service methods based on that parameter.
For example in my Repository I have abstract methods like these:
Page<Post> findAllByProvince(String province, Pageable pageable);
Page<Post> findAllByCity(String city, Pageable pageable);
Page<Post> findAllByProvinceAndCity(String province, String city, Pageable pageable);
Page<Post> findAllByBrandInAndProvince(List<String> brand, String province, Pageable pageable);
And I'd need much more so I could handle the other potential values, ie. findAllByPriceBetween(), findAllByCityAndPriceBetween(), findAllByProvinceAndPriceBetween()...
So I'd like some suggestions on how to handle this?.
Edit
Managed to get it working by overriding the toPredicate method as shown by #M. Deinum with some small tweaks according to my use case.
#Override
public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
// min/max is never not set as they have default values
predicates.add(builder.between(root.get("price"), params.getMinValue(), params.getMaxValue()));
if (params.getProvince() != null) {
predicates.add(builder.equal(root.get("province"), params.getProvince()));
}
if (params.getCity() != null) {
predicates.add(builder.equal(root.get("city"), params.getCity()));
}
if (!CollectionUtils.isEmpty(params.getBrand())) {
Expression<String> userExpression = root.get("brand");
Predicate p = userExpression.in(params.getBrand());
predicates.add(p);
}
return builder.and(predicates.toArray(new Predicate[0]));
}
Create an object to hold your variables instead of individual elements.
Move the logic to your service and pass the object and pageable to the service
Ditch those findAll methods from your repository and add the JpaSpecificationExecutor in your extends clause.
In the service create Predicate and use the JpaSpecificationExecutor.findAll to return what you want.
public class PostSearchParameters {
private String province;
private String city;
private List<String> brand;
private int minValue = 0;
private int maxValue = 500000;
//getters/setters or when on java17+ use a record instead of class
}
Predicate
public class PostSearchParametersSpecification implements Specification {
private final PostSearchParameters params;
PostSearchParametersPredicate(PostSearchParameters params) {
this.params=params;
}
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
// min/max is never not set as they have default values
predicates.add(builder.between(root.get("price", params.getMinValue(), params.getMaxValue());
if (params.getProvince() != null) {
predicates.add(builder.equal(root.get("province"), params.getProvince());
}
if (params.getCity() != null) {
predicates.add(builder.equal(root.get("city"), params.getCity());
}
if (!CollectionUtils.isEmpty(params.getBrand()) {
predicates.add(builder.in(root.get("brand")).values( params.getBrand());
}
return builder.and(predicates.toArray(new Predicate[0]));
}
}
Repository
public interface PostRepository extends JpaRepository<Post, Long>, JpaSpecificationExecutor<Post> {}
Service method
public Page<Post> searchPosts(PostSearchParameters params, Pageable pageSort) {
PostSearchParametersSpecification specification =
new PostSearchParametersSpecification(params)
return repository.findAll(specification, pageSort);
}
Now you can query on all available parameters, adding one is extending/modifying the predicate and you are good to go.
See also the Spring Data JPA Reference guide on Specifications

How to change the formatting of the output of Hibernate HQL query

I'm developing a Spring Boot application with Spring Data JPA. I'm using a custom JPQL query to group by some field and get the count. Following is my repository method.
#Query("SELECT v.status.name, count(v) as cnt FROM Pet v GROUP BY v.status.name")
List<Object[]> countByStatus();
It's working and result is obtained as follows:
[
[
"pending",
1
],
[
"available",
4
]
]
However, I would like my Rest endpoint to respond with an output which is formatted like this
{
"pending": 1,
"available": 4
}
How can I achieve this?
Basically you want to produce a JSON where its properties ("pending", "available") are dynamic and come from the SELECT v.status.name part of the query.
Create a DTO to hold the row values:
package com.example.demo;
public class ResultDTO {
private final String key;
private final Long value;
public ResultDTO(String key, Long value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Long getValue() {
return value;
}
}
Change your query to create a new ResultDTO per row:
#Query("SELECT new com.example.demo.ResultDTO(v.status.name, count(v)) as cnt FROM Pet v GROUP BY v.status.name")
List<ResultDTO> countByStatus();
"com.example.demo" is my package, you should change it to yours.
Then from your service class or from your controller you have to convert the List<ResultDTO> to a Map<String, Long> holding all rows' keys and values.
final List<ResultDTO> repositoryResults = yourRepository.countByStatus();
final Map<String, Long> results = repositoryResults.stream().collect(Collectors.toMap(ResultDTO::getKey, ResultDTO::getValue));
Your controller should be able to transform final Map<String, Long> results to the desired JSON

Best approach to create different JSON response from same Entity

I have an entity class User with 20 fields, some of them being confidential fields. I have a controller class, which has a method getUser to fetch all the user from DB and send the JSON respone. Below is the sample code for the same:
#GetMapping("/getUsers")
public UserDT getUsers( Model theModel) {
List<User> userList;
userList = userService.findAll();
return userList;
}
When I run the above code, it returns all the fields from User table/User Entity Class. Instead of sending all the fields, I would like to send selected fields say Field1 to Field5 only.
Ultimate goal is to have multiple views for the same Entity Class. For URL1 I would like to show only field1 to field5 of User table, But for URL2 I would like to show Field9 , Filed15, Field20.
Do I need to create multiple Entity Class for each URL? Please guide me with the best practice to be followed in such scenario.
Assuming you are using Spring Data JPA, use projections.
So create different projections for your different URLs write a method that returns the projection (or a dynamic one as in the documentation).
public interface NamesOnlyProjection {
String getFirstName();
String getLastName();
}
public interface UserinfoProjection {
String getUsername();
String getPassword();
String getDepartment();
}
Then in your repository do something like this
public interface PersonRepository extends JpaRepository<Person, Long> {
<T> List<T> findAll(Class<T> type);
}
Then you can do something like this in your controller/service
#RestController
public class PersonController {
private final PersonRepository persons;
#GetMapping("/people/names")
public List<NamesOnlyProjection> allNames() {
return persons.findAll(NamesOnlyProjection.class);
}
#GetMapping("/people/users")
public List<UserinfoProjection> allNames() {
return persons.findAll(UserinfoProjection.class);
}
}

specification-arg-resolver - URL operators

Looking at the sample code from specification-argument-resolver,
#Controller
public class SampleController {
#Autowired
CustomerRepository customerRepository;
#RequestMapping("/find")
public List<Customer> findByRegistrationDate(
#And({
#Spec(path = "name", params = "name", spec = Equal.class),
#Spec(path = "registrationDate", params = "registrationDate", spec = GreaterThanOrEqual.class)
}) Specification<Customer> spec) {
return customerRepository.findAll(spec);
}
}
#Entity
public class Customer {
String name;
Date registrationDate;
}
Looking at the sample code from specification-argument-resolver, the sample request looks like below:
GET /find?name=John&registrationDate=2020-06-19
Is there a way for the query params to reflect the actual operator.
Example: registrationDate>=2020-06-19 instead of just =
I am looking for a hybrid between RSQL and specification-argument-resolver.
You may just use this library: https://github.com/turkraft/spring-filter
It will let you run search queries such as:
/search?filter= average(ratings) > 4.5 and brand.name in ('audi', 'land rover') and (year > 2018 or km < 50000) and color : 'white' and accidents is empty
It supports dates too, and manages very well the n+1 query problem.

Spring Boot : Using JPA want to get the unique value from table

I have a table
CatID | CategoryName | scatID | subCategoryName
2 User 1 x
2 User 2 y
2 User 3 z
2 User 4 a
2 User 5 b
I am able to get all the value in JSON formate using SpringBoot.
My Requirement :
I want to get the unique CategoryName attribute but in current scenario I am getting all User coming 5 times.
I am looking for solution. Please any one can help to get over using Spring Boot JPA implementation.
You can use the Query annotation in your repository interface.
For example, below snippet code return all distinct categoryName.
Declare a simple bean class:
package com.example.test;
public class CategoryNameClass {
private String CategoryName;
public CategoryNameClass(String CategoryName) {
this.CategoryName = CategoryName;
}
public String getCategoryName() {
return CategoryName;
}
public void setCategoryName(String categoryName) {
CategoryName = categoryName;
}
}
Then use the below query:
public interface ARepo extends JpaRepository<A, String> {
#Query("SELECT DISTINCT new com.example.test.CategoryNameClass(a.categoryName) FROM A a ")
List<CategoryNameClass> findAllCategoryName();
}

Resources