Home > front end >  How to create multi parameter filter in Spring Boot and MongoDB?
How to create multi parameter filter in Spring Boot and MongoDB?

Time:11-28

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"!:

    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?

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

  • Related