18

I am using Spring Data for the paging and the sorting. However, I would like to perform multi-columns searches.

Now, I am using the annotation @Query in my repository interface like this:

public interface MyRepository extends PagingAndSortingRepository<Item,Long> {

    @Query(value="select mt from MY_TABLE mt where mt.field1 = %searchtext% or mt.field2 = %searchtext% or mt.field3 = %searchtext%")    
    Page<Item> findByAllColumns(@Param("searchtext") String searchtext, Pageable pageable);

}

I would like to know if there is another way to do because the number of columns in a table can be high.

Thanks for your help.

edit (clarification of the question after the comment of Brandon Oakley): The problem in this solution is in the where clause of the @Query annotation because we have to repeat the exact same searchtext parameter for every column we want to search on

jplandrain
  • 2,278
  • 4
  • 24
  • 24
  • This may answer your question... http://stackoverflow.com/a/20121813/2267073 – Brandon Oakley Sep 16 '14 at 18:47
  • I am not sure how: the problem in my current solution is in the where clause of the _@Query_ annotation because we have to repeat the exact same _searchtext_ parameter for every column we want to search on. So I wonder if there is another more elegant way to do that would allow us to just give the name of the columns. – jplandrain Sep 17 '14 at 09:32
  • 1
    Your answer is very interesting, but for other situations. So, thank you for the suggestion. – jplandrain Sep 17 '14 at 09:44

5 Answers5

14

Here is sample of such Specification for User:

public static Specification<User> containsTextInName(String text) {
    if (!text.contains("%")) {
        text = "%" + text + "%";
    }
    String finalText = text;
    return (root, query, builder) -> builder.or(
            builder.like(root.get("lastname"), finalText),
            builder.like(root.get("firstname"), finalText)
    );
}

or even more customizable implementation:

public static Specification<User> containsTextInAttributes(String text, List<String> attributes) {
    if (!text.contains("%")) {
        text = "%" + text + "%";
    }
    String finalText = text;
    return (root, query, builder) -> builder.or(root.getModel().getDeclaredSingularAttributes().stream()
            .filter(a -> attributes.contains(a.getName()))
            .map(a -> builder.like(root.get(a.getName()), finalText))
            .toArray(Predicate[]::new)
    );
}

public static Specification<User> containsTextInName(String text) {
    return containsTextInAttributes(text, Arrays.asList("lastname", "firstname"));
}

Usage:

userRepository.findAll(Specifications.where(UserSpecifications.containsTextInName("irs")))
Mykhailo Lytvyn
  • 191
  • 1
  • 5
  • 5
    just to note: `Specifications` is now deprecated in favour of `Specification`. – Deniss M. Mar 21 '19 at 10:10
  • 1
    @DenissM. - Do you know we should use instead ? Could you please share some info? How you were able to fixed this? – PAA Jul 19 '19 at 12:18
11

You could use specifications. That also gives you more flexibility. You can have one method, but use multiple specifications for a query:

Page<Item> findAll(Specification<T> spec, Pageable pageable);

myRepository.findAll(textInAllColumns(searchText), pageable);
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
a better oliver
  • 26,330
  • 2
  • 58
  • 66
  • Thanks for the pointer. We are currently investigating to check if that fits our need. This blog post is very interesting: http://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/ – jplandrain Sep 18 '14 at 07:42
  • That works fine and it is exactly what we were looking for: we don't even need to specify the _Query_ annotation anymore. And Pagination still works fine. And it's object oriented (i.e. perfectly reusable). Thank you very much ! – jplandrain Sep 18 '14 at 08:42
  • Although we haven't finally used it, this related project is also very interesting: http://blog.kaczmarzyk.net/2014/03/23/alternative-api-for-filtering-data-with-spring-mvc-and-spring-data/ – jplandrain Sep 18 '14 at 08:49
  • 2
    Do you have an example implementation of the "textInAllColumns" specification builder? – pioto Apr 29 '15 at 20:21
  • Why can't we use ExampleMatcher https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example.matchers – Pavan Jadda Oct 01 '20 at 16:09
  • 1
    @Jadda Query by example was not supported in 2014 ^^ But yes, it can be an alternative to specifications. – a better oliver Oct 02 '20 at 13:14
  • @Jadda Exactly "a better oliver", my question dates back to 2014. A lot has changed since then. But thanks for the comment, it's valid now. – jplandrain Oct 07 '20 at 15:58
9

Combining previous two answers: if you don't want to couple your API and your database schema or in other words you don't want the user to provide a string column name - you can filter out those attributes that are not strings and apply like to all those that are. In the following example it will try to search text in values of columns: name,field1, field2 and field3.

Entity Example:

@Entity
public class MyEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public int id;
    public String name;
    public String field2;
    public String field3;
    public String field4;
}

Specification Example:

public class EntitySpecification {

    public static Specification<MyEntity> textInAllColumns(String text) {

        if (!text.contains("%")) {
            text = "%"+text+"%";
        }
        final String finalText = text;

        return new Specification<MyEntity>() {
            @Override
            public Predicate toPredicate(Root<MyEntity> root, CriteriaQuery<?> cq, CriteriaBuilder builder) {
                return builder.or(root.getModel().getDeclaredSingularAttributes().stream().filter(a-> {
                    if (a.getJavaType().getSimpleName().equalsIgnoreCase("string")) {
                        return true;
                    }
                    else {
                        return false;
                }}).map(a -> builder.like(root.get(a.getName()), finalText)
                    ).toArray(Predicate[]::new)
                );
            }
        };
    }

 }

Repository Example:

public interface MyEntityRepository extends PagingAndSortingRepository<MyEntity, Integer> {
    List<MyEntity> findAll(Specification<MyEntity> spec);
}

Usage example:

List<MyEntity> res = failureRepository.findAll(Specifications.where(FailureSpecification.textInAllColumns(text)));

another update (search in all types of columns with white-listing of fields with lambdas - code is not checked)

public class EmployeeSpecification {
    public static Specification<Employee> textInAllColumns(String text, Set<String> fields) {
        if (!text.contains("%")) {
            text = "%" + text + "%";
        }
        final String finalText = text;

        return  (Specification<Employee>) (root, query, builder) -> 
                builder.or(root.getModel().getDeclaredSingularAttributes().stream().filter(a -> {
                return fields.contains(a.getName());
            }).map(a -> builder.like(root.get(a.getName()), finalText)).toArray(Predicate[]::new));
    }
} 
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • 1
    Solid answer. Do you know if there is a way to build the predicate in Java 7/without using streams? – Francis Bartkowiak Jun 28 '18 at 17:50
  • 1
    Thanks! Maybe something like this: https://dzone.com/articles/java-using-specification ? It should be possible to do a for loop over attributes to do the filtering and mapping. it is a matter if `root.getModel().getDeclaredSingularAttributes()` is available for older versions of Java/Spring Data – Michail Michailidis Jun 29 '18 at 07:32
  • just to note: `Specifications` is now deprecated in favour of `Specification`. – Deniss M. Mar 21 '19 at 10:10
  • @MichailMichailidis - It look like Specifications has been deprecated, is there any alternative for the same ? New Question here: https://stackoverflow.com/questions/57112319/spring-data-jpa-the-type-specificationst-is-deprecated – PAA Jul 19 '19 at 12:17
  • Thanks for the great answer but one thing is that the search will only work with the column type String. For numbers like Double and long datatype, this search will not work. Any idea? – Kishan Solanki Apr 19 '20 at 19:33
  • for those cases I usually create @Formula containing any info I want to search and those can be used as actual fields existing on the database even though they arent. Why would you need a text search like mechanism for number fields? even sql doesnt support that - for those other fields there are range queries or exact matches as well – Michail Michailidis Apr 19 '20 at 21:42
2

If you want to achieve,

1. Pagination,

2. Search in all String columns,

3. Sort By,

4. Sorting order

in same service/request then this is for you!

I am really impressed with Michail Michailidis' answer and I did update it in my way so that it can be used for any Entity with Pagination (with page number and page size dynamic), sort by, sort order etc.

First of all copy this class at your end:

    public class EntitySpecification {

    public static <T> Specification<T> textInAllColumns(String text) {
        if (!text.contains("%")) {
            text = "%" + text + "%";
        }
        final String finalText = text;

        return (Specification<T>) (root, cq, builder) ->
                builder.or(root.getModel()
                        .getDeclaredSingularAttributes()
                        .stream()
                        .filter(a -> a.getJavaType()
                                .getSimpleName().equalsIgnoreCase("string"))
                        .map(a -> builder.like(root.get(a.getName()), finalText)
                        ).toArray(Predicate[]::new)
                );
    }
}

Now, in your service class, for example in your UserService class if you want to achieve something like users list along with search, sort, pagination etc, then use this only

Pageable paging;
    if (paginationRequest.getSortOrder().matches("ASC")) {
        paging = PageRequest.of(paginationRequest.getPageNo(),
                paginationRequest.getPageSize(), Sort.by(
                        paginationRequest.getSortBy()).ascending());
    } else {
        paging = PageRequest.of(paginationRequest.getPageNo(),
                paginationRequest.getPageSize(), Sort.by(paginationRequest.getSortBy()).descending());
    }

    List<User> userList = userRepository.findAll(
            EntitySpecification.textInAllColumns(paginationRequest.getSearch())
            , paging).getContent();

Now don't get confused here,

PaginationRequest is request POJO class with getters and setters having following initially,

Integer pageNo = 0;
Integer pageSize = 10;
String sortBy = "createdTimeStamp";
String sortOrder;
String search = "";
Kishan Solanki
  • 13,761
  • 4
  • 85
  • 82
0

All above the solutions are great, but we can also use Example and ExampleMatcher for multi column search

  1. First define search object with search parameters
  2. Second, define Custom Example Matcher using ExampleMatcher and Example
  3. Third, use customExampleMatcher in findAll() method
/* Build Search object */
Employee employee=new Employee();
        employee.setFirstName(requestDTO.getFilterText());
        employee.setLastName(requestDTO.getFilterText());
        employee.setEmail(requestDTO.getFilterText());

/* Build Example and ExampleMatcher object */
ExampleMatcher customExampleMatcher = ExampleMatcher.matchingAny()
                .withMatcher("firstName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                .withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                .withMatcher("email", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());

Example<Employee> employeeExample= Example.of(employee, customExampleMatcher);

/* Get employees based on search criteria*/
employeetRepository.findAll(employeeExample, PageRequest.of(requestDTO.getCurrentPageNumber(), requestDTO.getPageSize(), Sort.by(requestDTO.getSortingOrderColumnName()).descending()));

Pavan Jadda
  • 4,306
  • 9
  • 47
  • 79
  • This answer is valid. However, my question dates back to 2014 when this wasn't possible yet. – jplandrain Oct 07 '20 at 16:00
  • What if you don't want hard written column names? For maintainability purposes later if column names are edited or added it would probably break or it would be missing columns. – Merv Dec 21 '21 at 13:05
  • @Merv If column names changed, one should edit DAO for adaption. JPA uses `@Column` annotation so it can support different DB column name v.s. DAO field name - if the project has a correct DAO modeling, though. – Xiang Wei Huang Jul 14 '22 at 04:02
  • I understand. I was thinking more of a dynamic approach where you can choose your columns dynamically and not hardcode your columns. – Merv Jul 14 '22 at 12:51