I am trying to create simple online library to learn more about Spring Boot and MongoDB. I wanted to create filter that uses one or more parametrs.
Like this: The customer list will be searchable by first name, last name, address, birth number. The user can search using one or more parameters. It will also be possible to sort by these attributes. All text searches will be functional from the specified three characters and can be any part of the text (in the infix, prefix or suffix of a string).
My questions:
1 - How can I create multi parameter filter? The only thing that comes to my mind is like this: In UserRepository:
public findByFirstNameAndLastNameAndAdressAndBirthNumber(String firstName, String lastName, String address Long birthNumber)
But it doesnt seem right to me.
2 - How can I make all of the parameters optional since user can search other users by one or more params?
3 - How should the endpoint for this request look like? Again, the only solution that comes to my mind is: http://localhost:8080/users/{firstName}/{lastName}/{address}/{birthNumber} -> but how can I handle the endpoint if only one of this parameters will be present?
4 - How to create filter that works with the metioned infix, prefix, ... ?
CodePudding user response:
Use MongoTemplate
and Criteria
API to create the queries dynamically:
Criteria criteria = new Criteria();
if (firstName != null) {
criteria = criteria.and("firstName").is(firstName);
}
if (lastName != null) {
criteria = criteria.and("lastName").is(lastName);
}
Query query = new Query(criteria);
List<Customer> customer = mongoTemplate.find(query, Customer.class);
For matching prefix suffixes and infix, use regex
:
criteria.and("firstName").regex(".*name.*")
and finally, to sort dynamically:
query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "firstName"),
new Sort.Order(Sort.Direction.DESC, "lastName")));
Maven dependency:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
Regarding the REST API, you could use query params instead of path variables to handle the case where only one of these parameters is present:
http://localhost:8080/users?firstName=name
CodePudding user response:
Answers
1 - How can I create multi parameter filter?
-
Query by Example is well suited for several use cases:
- Querying your data store with a set of static or dynamic constraints.
- Frequent refactoring of the domain objects without worrying about breaking existing queries.
- Working independently from the underlying data store API.
, it's "limitations" sound more like "features" in this "scenario":
Query by Example also has several limitations:
- No support for nested or grouped property constraints, such as
firstname = ?0 or (firstname = ?1 and lastname = ?2).#(we must suffice with "all
AND
/OR
".) - Only supports starts/contains/ends/regex matching for strings...#(which is super)
- ... and exact matching for other property types.#;(;(
(see "Hands On")
2 - How can I make all of the parameters optional
since user can search other users by one or more params?
In controller, just like Spring MVC: Complex object as GET @RequestParam :
@RequestMapping("/users") public ResponseDto users(RequestDto pInput // ...
In query, by configuring a "correct"
Example
. (see "Hands On")
3 - How should the endpoint for this request look like?
Again, the only solution that comes to my mind is:
http://localhost:8080/users/{firstName}/{lastName}/{address}/{birthNumber}
....
No Way! Please don't use "path variables" for "nullable parameters" (neither for too fancy things, e.g. Strings containing
/
...). (See "2 - How can I...", "Hands On")It can rather (for
GET
requests) look like:http://localhost:8080/users?firstname=John&lastname=Doe&sort..&mode...&page...
or just like:
http://localhost:8080/users
.
4 - How to create filter that works with the metioned infix, prefix, ... ?
- by configuring a "correct"
Example
. (see "Hands On";)
Hands On
- Starter(dependencies=lombok,web,data-mongodb)
- Assume (according/similar to 1):
package com.example.mongoqbe; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.Id; @Getter @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor public class Person { @Id private String id; private String firstname; private String lastname; private Address address; @AllArgsConstructor(staticName = "of") @NoArgsConstructor @Getter @Setter static class Address { String zipCode, city, street; } }
- Repository (just):
package com.example.mongoqbe; import org.springframework.data.mongodb.repository.MongoRepository; public interface PersonRepository extends MongoRepository<Person, String> { }
- Controller:
package com.example.mongoqbe; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController class DemoController { @Autowired PersonRepository personRepo; @GetMapping("users") public Page<Person> users( Person qPerson, // our probe, can be any "java bean" (with matching "properties";) Pageable pgbl, // cheap: full paging control of Repository @RequestParam(required = false, defaultValue = "") String mode // CUSTOM parameter, not required, default = "" ) { // CUSTOM matcher/method from CUSTOM param: ExampleMatcher myMatcher = myMatcherOf(mode); Example<Person> example = Example.of( qPerson == null ? new Person() : qPerson, myMatcher ); return personRepo.findAll( example, pgbl == null ? Pageable.unpaged() : pgbl ); } // e.g. but working, details important: private static ExampleMatcher myMatcherOf(String mode) { ExampleMatcher result = ExampleMatcher.matching() // i.e. "matchingAll" ("all AND") ..for "all OR": .matchingAny() .withIgnoreNullValues() // this makes sense for "all AND" // alternatively: .withIncludeNullValues() // customize more, see docs... ; switch (mode) { case "pre" -> result = result.withStringMatcher(ExampleMatcher.StringMatcher.STARTING); case "post" -> result = result.withStringMatcher(ExampleMatcher.StringMatcher.ENDING); case "in" -> result = result.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING); // more cases/tuning ..as we like/need (REGEX!, ignore case!?). default -> { // i.e. (when not specified) "store specific": // -> ExampleMatcher.StringMatcher.DEFAULT } } return result; } }
Testing Time
Main/App (with little test data):
package com.example.mongoqbe; import java.util.UUID; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class MongoQbeApplication { public static void main(String[] args) { SpringApplication.run(MongoQbeApplication.class, args); } @Bean public InitializingBean testData(PersonRepository repo) { return () -> { // Don't do this on prod! :)) repo.deleteAll(); repo.save( Person.of( UUID.randomUUID().toString(), "John", "Doe", Person.Address.of( "02861", "Pawtucket", "214 Columbus Ave" ) ) ); repo.save( Person.of( UUID.randomUUID().toString(), "Jane", "Does", Person.Address.of( "11801", "Hicksville", "55 Broadway" ) ) ); }; }
(Integration) Test:
(Mongo DB on
mogo:localhost/test
is up and available)package com.example.mongoqbe; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @AutoConfigureMockMvc class MongoQbeApplicationTests { @Autowired MockMvc mvc; @Test void testUsersNoParams() throws Exception { mvc .perform(get("/users")) .andDo(print()) .andExpectAll( status().isOk(), header().string( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE ), jsonPath("$.content").isArray(), jsonPath("$.content[0]").isMap(), jsonPath("$.content[1]").isMap(), jsonPath("$.pageable").isMap(), jsonPath("$.sort").isMap(), jsonPath("$.pageable.totalElements").value(greaterThanOrEqualTo(2)) // , ... more asserts on "test data" ); } // ... also with more parameters: @Test void testUsersFirstnamePrefixAndSort() throws Exception { mvc .perform( get("/users") .param("mode", "pre") // "prefix mode" .param("firstname", "J") // firstname starts with 'J' .param("sort", "address.zipCode,desc") // order by address.zipCode descending ) .andDo(print()) .andExpectAll( status().isOk(), jsonPath("$.sort.sorted").value(true), jsonPath("$.content[0].firstname").value(equalTo("Jane")), jsonPath("$.content[1].firstname").value(equalTo("John")) ); } }
Try it in browser/postman/curl
(Db App up & running):
curl -X GET "http://localhost:8080/users? firstname=J&mode=pre&sort=address.zipCode,desc"
responds:
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 632 0 632 0 0 65 0 --:--:-- 0:00:09 --:--:-- 182 { "content":[ { "id":"17957c0f-c367-4ddd-b9ba-3027af446dac", "firstname":"Jane", "lastname":"Does", "address":{ "zipCode":"11801", "city":"Hicksville", "street":"55 Broadway" } },{ "id":"0da6a3e2-f92c-4f36-b6f6-dbd5b99b70c4", "firstname":"John", "lastname":"Doe", "address":{ "zipCode":"02861", "city":"Pawtucket", "street":"214 Columbus Ave" } } ], "pageable":{ "sort":{ "empty":false, "sorted":true, "unsorted":false }, "offset":0, "pageNumber":0, "pageSize":20, "paged":true, "unpaged":false }, "last":true, "totalPages":1, "totalElements":2, "size":20, "number":0, "sort":{ "empty":false, "sorted":true, "unsorted":false }, "first":true, "numberOfElements":2, "empty":false }
References
- HTTP (query) Parameters(rfc#http/1.1)
QueryByExampleExecutor
(javadoc|src)Example
(javadoc|src)