DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Minimizing Latency in Kafka Streaming Applications That Use External API or Database Calls
  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable
  • A Guide to Enhanced Debugging and Record-Keeping
  • Design to Support New Query Parameters in GET Call Through Configurations Without Making Code Changes

Trending

  • A Guide to Developing Large Language Models Part 1: Pretraining
  • Stateless vs Stateful Stream Processing With Kafka Streams and Apache Flink
  • Breaking Bottlenecks: Applying the Theory of Constraints to Software Development
  • *You* Can Shape Trend Reports: Join DZone's Software Supply Chain Security Research
  1. DZone
  2. Data Engineering
  3. Databases
  4. Client Oriented Dynamic Search Query Supporting Multiple Tables in Spring

Client Oriented Dynamic Search Query Supporting Multiple Tables in Spring

The main motive of this article to have a simple and common data search logic that applies to almost every table and is client-oriented.

By 
Ashish Lohia user avatar
Ashish Lohia
DZone Core CORE ·
Apr. 21, 20 · Analysis
Likes (3)
Comment
Save
Tweet
Share
21.4K Views

Join the DZone community and get the full member experience.

Join For Free

Backdrop

To begin with, this an example primarily written in Springboot to leverage the benefits of Spring Data JPA. The main motive of this article to have a simple and common data search logic that applies to almost every table and is client-oriented. This article is heavily inspired by one from Eugen Paraschiv, I recommend going through his tutorials to learn Spring professionally.

Pre-Requisites for Getting Started

  • Java 8 is installed.
  • Any Java IDE (preferably STS or IntelliJ IDEA).
  • Basic understanding of Java and Spring-based web development along with Spring Data JPA.

I used Spring Initializer to add all the dependencies and create a blank working project with all my configurations. I used Maven as project build type and Java 8 as language, though this part is up to your choice as long as it is supported by spring. Below are my required dependencies which can easily be added from spring initializer. 

I am using H2 but any other database with JPA support should work well. I have also used Lombok to generate some general java code snippets on compile-time, I highly recommend by can be skipped, but don't forget to install it in your IDE else the compilation errors will keep coming.

Spring boot starters

Below is my project structure, it is just like any other Spring Boot Project.
jpa-search
Let's start with the entities since our main goal is to search tables we need to create some entities based on the tables.

Java
 




xxxxxxxxxx
1
26


 
1
@Entity
2
@Getter
3
@Setter
4
@NoArgsConstructor
5
public class Avenger {
6
    @Id
7
    @GeneratedValue(strategy = GenerationType.AUTO)
8
    private long id;
9
    private String firstName;
10
    private String lastName;
11
    private String alias;
12
    private int age;
13
    private BigDecimal powerRating;
14
    private boolean seniorMembers;
15
    private Instant joiningDate = Instant.now();
16
 
           
17
    public Avenger(String firstName, String lastName, String alias, int age, BigDecimal powerRating,
18
            boolean seniorMembers) {
19
        this.firstName = firstName;
20
        this.lastName = lastName;
21
        this.alias = alias;
22
        this.age = age;
23
        this.powerRating = powerRating;
24
        this.seniorMembers = seniorMembers;
25
    }
26
}
27
 
           
28
@Entity
29
@Getter
30
@Setter
31
@NoArgsConstructor
32
public class JusticeLeaguer {
33
    @Id
34
    @GeneratedValue(strategy = GenerationType.AUTO)
35
    private long id;
36
    private String firstName;
37
    private String lastName;
38
    private String alias;
39
    private int age;
40
    private BigDecimal powerRating;
41
    private boolean seniorMembers;
42
    private Instant joiningDate = Instant.now();
43
 
           
44
    public JusticeLeaguer(String firstName, String lastName, String alias, int age, BigDecimal powerRating,
45
            boolean seniorMembers) {
46
        this.firstName = firstName;
47
        this.lastName = lastName;
48
        this.alias = alias;
49
        this.age = age;
50
        this.powerRating = powerRating;
51
        this.seniorMembers = seniorMembers;
52
    }
53
}


As we can see we have two tables of Superheroes belonging to two different universes (yes I like Superheroes, grow up :p). I won't go much into the details of creating the entities as these are pretty straightforward, still would be glad to resolve any doubts. Now, we will try to create a dynamic query generator using the JPA Specification and javax Criteria package classes.

The next step is to create repositories for the above entities.

Java
 




x


 
1
@Repository
2
public interface AvengerRepository extends CrudRepository<Avenger, Long>, JpaSpecificationExecutor<Avenger>{
3
 
          
4
}
5
 
          
6
@Repository
7
public interface JusticeLeaguerRepository extends CrudRepository<JusticeLeaguer, Long>, JpaSpecificationExecutor<JusticeLeaguer>{
8
 
          
9
}



As we can see, we extended two interfaces:

CrudRepository

As the name suggests this Repository provides CRUD utilities on the table.

JpaSpecificationExecutor

This interface is needed to pass the specifications created by the CriteriaBuiler to filter the datasets along with paging and sorting which we will see later as well.

Now we are ready to pass certain parameters to our repository methods to get desired results. But, our main objective was to create a query engine where the query generation logic will completely generic and applicable for most of the tables. Let's take a step back and come up with an approach.

 Let's create a POJO which will all the details we need to create one specification.

Java
 




x
16


 
1
@Data
2
public class SearchCriteria {
3
    private boolean clauseAnd;
4
    private SearchParamTypeEnum type;
5
    private String key;
6
    private String operation;
7
    private String value;
8
    
9
    public SearchCriteria(String clause, String type, String key, String operation, String value) {
10
        this.clauseAnd = StringUtils.isEmpty(clause);
11
        this.type = SearchParamTypeEnum.valueOfType(type);
12
        this.key = key;
13
        this.operation = operation;
14
        this.value = value;
15
    }
16
}



Let's see what each field signifies:

  • ClauseAnd: This denotes whether the upcoming specification is an AND clause or an OR clause.
  • Type: This is an enum field that identifies which type of column we filtering on. It might be Long, String, Instant, Boolean, etc.
  • Key: This is the name of the field, give special care on naming your variable and the same need to passed in the query.
  • Operation: This is the type of query operation we need to do. e.g >, <, =, in etc.
  • Value: The value of the field we want to query on.

This bean will now be converted to the corresponding specification.

Java
 




x
73


 
1
@NoArgsConstructor
2
@AllArgsConstructor
3
public class SearchSpecification<T> implements Specification<T> {
4
 
          
5
    private static final long serialVersionUID = -0x2733DE2E86ED0B65L;
6
    private SearchCriteria criteria;
7
 
          
8
    @Override
9
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
10
        switch (criteria.getType()) {
11
        case NUM_PARAM: {
12
            if (criteria.getOperation().equals(G_T))
13
                return criteriaBuilder.gt(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
14
            if (criteria.getOperation().equals(L_T))
15
                return criteriaBuilder.lt(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
16
            if (criteria.getOperation().equals(EQUALS))
17
                return criteriaBuilder.equal(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
18
            if (criteria.getOperation().equals(NOT_EQUALS))
19
                return criteriaBuilder.notEqual(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
20
            if (criteria.getOperation().equals(G_T_EQUALS))
21
                return criteriaBuilder.ge(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
22
            if (criteria.getOperation().equals(L_T_EQUALS))
23
                return criteriaBuilder.le(root.get(criteria.getKey()), new BigDecimal(criteria.getValue()));
24
            if (criteria.getOperation().equals(IN)) {
25
                In<Object> in = criteriaBuilder.in(root.get(criteria.getKey()));
26
                for (String str : criteria.getValue().split(COMMA))
27
                    in.value(new BigDecimal(str));
28
                return in;
29
            }
30
        }
31
            return null;
32
        case DATE_TIME_PARAM: {
33
            if (criteria.getOperation().equals(G_T))
34
                return criteriaBuilder.greaterThan(root.get(criteria.getKey()), parse(criteria.getValue()));
35
            if (criteria.getOperation().equals(L_T))
36
                return criteriaBuilder.lessThan(root.get(criteria.getKey()), parse(criteria.getValue()));
37
            if (criteria.getOperation().equals(EQUALS))
38
                return criteriaBuilder.equal(root.get(criteria.getKey()), parse(criteria.getValue()));
39
            if (criteria.getOperation().equals(NOT_EQUALS))
40
                return criteriaBuilder.notEqual(root.get(criteria.getKey()), parse(criteria.getValue()));
41
            if (criteria.getOperation().equals(G_T_EQUALS))
42
                return criteriaBuilder.greaterThanOrEqualTo(root.get(criteria.getKey()), parse(criteria.getValue()));
43
            if (criteria.getOperation().equals(L_T_EQUALS))
44
                return criteriaBuilder.lessThanOrEqualTo(root.get(criteria.getKey()), parse(criteria.getValue()));
45
            if (criteria.getOperation().equals(IN)) {
46
                In<Object> in = criteriaBuilder.in(root.get(criteria.getKey()));
47
                for (String str : criteria.getValue().split(COMMA))
48
                    in.value(parse(str));
49
                return in;
50
            }
51
        }
52
            return null;
53
        case STRING_PARAM: {
54
            if (criteria.getOperation().equals(EQUALS))
55
                return criteriaBuilder.like(root.<String>get(criteria.getKey()),
56
                        format(LIKE_PRE_POST, criteria.getValue()));
57
            if (criteria.getOperation().equals(NOT_EQUALS))
58
                return criteriaBuilder.notLike(root.<String>get(criteria.getKey()),
59
                        format(LIKE_PRE_POST, criteria.getValue()));
60
            if (!criteria.getOperation().equals(IN))
61
                return null;
62
            In<Object> in = criteriaBuilder.in(root.get(criteria.getKey()));
63
            for (String str : criteria.getValue().split(COMMA))
64
                in.value(str);
65
            return in;
66
        }
67
        case BOOLEAN_PARAM:
68
            return criteriaBuilder.equal(root.get(criteria.getKey()), parseBoolean(criteria.getValue()));
69
        default:
70
            return null;
71
        }
72
    }
73
}



As we can see we are implementing the Specification interface and overriding its method to return a specification based on the values of type and operation which we get from the SearchCriteria object. The values are coming from a Constant interface.

Java
 




xxxxxxxxxx
1
18


 
1
public interface Constants {
2
    Pattern SEARCH_QUERY_PATTERN = Pattern.compile("(OR-)?(N-|S-|D-|B-)(\\w+?)(=|<|>|<=|>=|#|!=)(\"([^\"]+)\")");
3
    String EMPTY = "";
4
    String DOUBLE_QUOTES = "\"";
5
    String COMMA = ",";
6
    String COLON = ":";
7
    String LIKE_PRE_POST = "%%%s%%";
8
    String EQUALS = "=";
9
    String L_T = "<";
10
    String NOT_EQUALS = "!=";
11
    String G_T = ">";
12
    String L_T_EQUALS = "<=";
13
    String G_T_EQUALS = ">=";
14
    String IN = "#";
15
    String DEFAULT_PROP = "joiningDate";
16
    int DEFAULT_PAGE_SIZE = 30;
17
    int DEFAULT_PAGE_INDEX = 0;
18
}



Our next step is to combine one or multiple specifications to create one combined query. Let's see how we can do that. Also, we need to have support for both AND and OR.

Java
 




xxxxxxxxxx
1
26


 
1
public class SearchSpecificationBuilder<T> {
2
    private final List<SearchCriteria> params;
3
 
          
4
    public SearchSpecificationBuilder() {
5
        params = new ArrayList<>();
6
    }
7
 
          
8
    public SearchSpecificationBuilder<T> with(String clause, String type, String key, String operation, String value) {
9
        params.add(new SearchCriteria(clause, type, key, operation, value));
10
        return this;
11
    }
12
 
          
13
    public Specification<T> build() {
14
        if (params.isEmpty())
15
            return null;
16
        Iterator<SearchCriteria> iterator = params.iterator();
17
        SearchCriteria criteria = iterator.next();
18
        Specification<T> result = Specification.where(new SearchSpecification<T>(criteria));
19
        while (iterator.hasNext()) {
20
            criteria = iterator.next();
21
            result = criteria.isClauseAnd() ? result.and(new SearchSpecification<T>(criteria))
22
                    : result.or(new SearchSpecification<T>(criteria));
23
        }
24
        return result;
25
    }
26
}



Look at line no. 21, how we are using the clauseAnd field to determine whether we want AND/OR.

The next step is to get the SearchCriteria from the client's end. We can expect a list of SearchCriteria directly or have some sort of query string and parse that String to extract it. This part of logic is up to you. I will go with a query string approach.

Suppose, we create a query string like this:

 "S-firstName#\"Tony,Steve\",B-seniorMembers=\"true\",

OR-N-age!=\"30\",D-joiningDate>\"2020-04-05T14:44:51.366Z\"" 

To parse this, we need to first separate based on a comma and pass through a pattern like this:

 "(OR-)?(N-|S-|D-|B-)(\\w+?)(=|<|>|<=|>=|#|!=)(\"([^\"]+)\")" 

Using Matcher.group we can extract each field information, pay attention to enclosing the values with double quotes to include special characters. Also, the operations should follow what we have added to our constants.

Java
 




xxxxxxxxxx
1


 
1
    public static <T> Specification<T> createSpec(String query) {
2
        SearchSpecificationBuilder<T> builder = new SearchSpecificationBuilder<T>();
3
        for (Matcher matcher = SEARCH_QUERY_PATTERN.matcher(query + COMMA); matcher.find();)
4
            builder.with(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4),
5
                    matcher.group(5).replaceAll(DOUBLE_QUOTES, EMPTY));
6
        return builder.build();
7
    }



Let's give a quick look at the overall search request body.

Java
 




xxxxxxxxxx
1


 
1
@Data
2
public class SearchReqDto {
3
    private int pageIndex = DEFAULT_PAGE_INDEX;
4
    private int pageSize = DEFAULT_PAGE_SIZE;
5
    private String query;
6
    private List<String> sorts = new ArrayList<String>();
7
}



In the request body apart from having the query we have 3 more fields which will help in providing sorting and pagination. One important thing here is how the sort is a list of String and how it helps in adding sorting. e.g.  "sorts":["powerRating","firstName:A"] 

Here the default sorting is in Descending order, in case we want ascending we need to append:A. Let's see how it works.

Java
 




xxxxxxxxxx
1
10


 
1
    public static List<Sort.Order> getOrders(List<String> sorts, String defaultProp) {
2
        if (sorts.isEmpty())
3
            return Arrays.asList(new Sort.Order(DESC, defaultProp));
4
        List<Sort.Order> orders = new ArrayList<Sort.Order>(sorts.size());
5
        for (String sort : sorts) {
6
            String[] split = sort.split(COLON);
7
            orders.add(new Sort.Order(split.length > 1 ? ASC : DESC, split[0]));
8
        }
9
        return orders;
10
    }



Let's take a look at our service class now and see how we can use our generic code to query two tables with much effort.

Java
 




x
57


 
1
@Service
2
public class HeroServiceImpl implements HeroService {
3
 
          
4
    private final AvengerRepository avengerRepository;
5
    private final JusticeLeaguerRepository justiceLeaguerRepository;
6
 
          
7
    @Autowired
8
    public HeroServiceImpl(AvengerRepository avengerRepository, JusticeLeaguerRepository JusticeLeaguerRepository) {
9
        this.avengerRepository = avengerRepository;
10
        this.justiceLeaguerRepository = JusticeLeaguerRepository;
11
    }
12
 
          
13
    @Override
14
    public SearchResDto searchMarvel(SearchReqDto reqDto) {
15
        PageRequest pageRequest = PageRequest.of(reqDto.getPageIndex(), reqDto.getPageSize(),
16
                by(getOrders(reqDto.getSorts(), DEFAULT_PROP)));
17
        Page<Avenger> page = avengerRepository.findAll(createSpec(reqDto.getQuery()), pageRequest);
18
        Function<Avenger, HeroDto> mapper = (hero) -> createCopyObject(hero, HeroDto::new);
19
        return prepareResponseForSearch(page, mapper);
20
    }
21
 
          
22
    @Override
23
    public SearchResDto searchDc(SearchReqDto reqDto) {
24
        PageRequest pageRequest = PageRequest.of(reqDto.getPageIndex(), reqDto.getPageSize(),
25
                by(getOrders(reqDto.getSorts(), DEFAULT_PROP)));
26
        Page<JusticeLeaguer> page = justiceLeaguerRepository.findAll(createSpec(reqDto.getQuery()), pageRequest);
27
        Function<JusticeLeaguer, HeroDto> mapper = (hero) -> createCopyObject(hero, HeroDto::new);
28
        return prepareResponseForSearch(page, mapper);
29
    }
30
 
          
31
    // create test data
32
    @PostConstruct
33
    public void createTestData() {
34
        avengerRepository.saveAll(asList(new Avenger("Tony", "Stark", "Iron Man", 40, new BigDecimal(9.8f), true),
35
                new Avenger("Bruce", "Banners", "Hulk", 35, new BigDecimal(8.2f), true),
36
                new Avenger("Steve", "Rogers", "Captain America", 32, new BigDecimal(9.9f), true),
37
                new Avenger("Clint", "Barton", "Hawkeye", 36, new BigDecimal(7.5f), true),
38
                new Avenger("Sam", "Wilson", "Falcon", 29, new BigDecimal(6.6f), false),
39
                new Avenger("Peter", "Parker", "Spiderman", 20, new BigDecimal(9), false),
40
                new Avenger("Nick", "Fury", "Fury", 45, new BigDecimal(8.1f), true),
41
                new Avenger("Scott", "Lang", "Antman", 37, new BigDecimal(8.3f), false),
42
                new Avenger("Nathasha", "Romannoff", "Black Widow", 33, new BigDecimal(9.2f), true),
43
                new Avenger("Thor", "Odinson", "Thor", 1025, new BigDecimal(9.9f), true),
44
                new Avenger("Wanda", "Maximoff", "Scarlett Witch", 26, new BigDecimal(9), false)));
45
        justiceLeaguerRepository
46
                .saveAll(asList(new JusticeLeaguer("Bruce", "Wayne", "BATMAN", 39, new BigDecimal(20), true),
47
                        new JusticeLeaguer("Clark", "Kent", "Superman", 32, new BigDecimal(10), true),
48
                        new JusticeLeaguer("Diana", "Prince", "Wonder Woman", 1051, new BigDecimal(10), true),
49
                        new JusticeLeaguer("Barry", "Allen", "Flash", 26, new BigDecimal(9.9f), true),
50
                        new JusticeLeaguer("Oliver", "Queen", "Green Arrow", 30, new BigDecimal(9), false),
51
                        new JusticeLeaguer("Hal", "Jordan", "Green Lantern", 31, new BigDecimal(9.55f), true),
52
                        new JusticeLeaguer("Jonn", "Jonnz", "Martian Manhunter", 745, new BigDecimal(9.2f), true),
53
                        new JusticeLeaguer("Billy", "Batson", "SHAZAM", 15, new BigDecimal(9.5f), false),
54
                        new JusticeLeaguer("Dinah", "Lang", "Black Canary", 28, new BigDecimal(8.5f), false),
55
                        new JusticeLeaguer("Kara", "Danvers", "Supergirl", 22, new BigDecimal(9.1f), false)));
56
    }
57
}



Examine how searchMarvel and searchDc are querying different tables without any specific logic apart from using a specific repository. For the sake of simplicity, I created two API but we can the same API for both cases with some minor conditions and passing another param quite easily. The last method is for generating test data which is again optional.

This is the core logic of creating a dynamic query engine. I have to say this example is to provide an idea and can be added with additional features and remove some as per your requirements. I hope it will provide some useful information.

I have left out the pagination part on purpose since it is nothing new and pretty self-explanatory.

I also have added some helper methods and DTO to create response data.

Java
 




x
14


 
1
    public static <T, R> SearchResDto prepareResponseForSearch(Page<T> page, Function<T, R> mapper) {
2
        SearchResDto response = new SearchResDto();
3
        response.setHeroes(page.stream().map(mapper).collect(toList()));
4
        response.setPageIndex(page.getNumber());
5
        response.setTotalPages(page.getTotalPages());
6
        response.setTotalRecords(page.getTotalElements());
7
        return response;
8
    }
9
 
          
10
    public static <T> T createCopyObject(Object src, Supplier<T> supplier) {
11
        T dest = supplier.get();
12
        copyProperties(src, dest);
13
        return dest;
14
    }



The full source code can be forked or downloaded from here.

Run this like any Springboot app in IDE or a jar after a build.

Sample request command:

JSON
xxxxxxxxxx
1
 
1
 curl -X POST \http://localhost:8080/hero/searchDc \-H 'cache-control: no-cache' \   -H 'content-type: application/json' \   -H 'postman-token: bef5c61b-62a0-0d88-b0df-9510f7c472a4' \   -d '{     "pageIndex":0,     "pageSize":5,     "query":"S-firstName#\"Tony,Steve\",B-seniorMembers=\"true\",OR-N-age!=\"30\",D-joiningDate>\"2020-04-05T14:44:51.366Z\"",     "sorts":["powerRating","firstName:A"] }' 


Feel free to post suggestions, improvements or queries, will be glad to work on those.

Database Spring Framework

Published at DZone with permission of Ashish Lohia, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Minimizing Latency in Kafka Streaming Applications That Use External API or Database Calls
  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable
  • A Guide to Enhanced Debugging and Record-Keeping
  • Design to Support New Query Parameters in GET Call Through Configurations Without Making Code Changes

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!