简体   繁体   中英

Pageable Sorting not working in Spring Data JPA, Spring Framework

I'm trying to implement a search feature across many attributes for a person.

Here's the model.

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Person {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private long id;
    
    private String firstName;
    
    private String surName;
    
    private int age;
    
    private Date DOB;
    
    private String description;
    
    private String highestEducationQualification;
    
    private String occupation;
    
    private String employer;
    
    private String college;
    
    private String school;
    
    private String eyecolor;
    
    private double weight;
    
    private double height;
    
    private String PPSnumber;
    
    private boolean driversLicence;
    
    private boolean provisionalLicence;
    
    private String bankIBAN;
    
    private long phoneNumber;
    
    private char gender;
    
    private String emailAddress;
    
    private String websiteAddress;
    
    private String homeAddress;
    
}

Here's my repository.

@Repository
public interface PersonRepo extends JpaRepository<Person, Long>{
    
    List<Person> searchByFirstNameContainingAllIgnoreCase(String firstName, Pageable page);
    List<Person> searchBySurNameContainingAllIgnoreCase(String surName, Pageable page);
    List<Person> searchByAge(int age, Pageable page);
    List<Person> searchByDescriptionContainingAllIgnoreCase(String desc, Pageable page);
    List<Person> searchByHighestEducationQualificationContainingAllIgnoreCase(String edu, Pageable page);
    List<Person> searchByOccupationContainingAllIgnoreCase(String occ, Pageable page);
    List<Person> searchByEmployerContainingAllIgnoreCase(String emp, Pageable page);
    List<Person> searchByCollegeContainingAllIgnoreCase(String emp, Pageable page);
    List<Person> searchBySchoolContainingAllIgnoreCase(String emp, Pageable page);
    List<Person> searchByEyecolorContainingAllIgnoreCase(String eye, Pageable page);
    List<Person> searchByWeight(double weight, Pageable page);
    List<Person> searchByHeight(double height, Pageable page);
    List<Person> searchByPPSnumberIgnoreCase(String emp, Pageable page);
    List<Person> searchByDriversLicence(boolean emp, Pageable page);
    List<Person> searchByProvisionalLicence(boolean emp, Pageable page);
    List<Person> searchByBankIBANAllIgnoreCase(String emp, Pageable page);
    List<Person> searchByPhoneNumber(long phone, Pageable page);
    List<Person> searchByGender(char emp, Pageable page);
    List<Person> searchByEmailAddressIgnoreCase(String emp, Pageable page);
    List<Person> searchByWebsiteAddressContainingAllIgnoreCase(String emp, Pageable page);
    List<Person> searchByHomeAddressContainingAllIgnoreCase(String emp, Pageable page);
}

Service function.

class PersonService {

    @Autowired
    private PersonRepo personRepo;

    @Override
    public List<Person> searchByAllAttributes(String toSearch, int page, int quan, String sortBy, boolean ascending) {
        int ageToSearch = 0;
        try {
            ageToSearch = Integer.parseInt(toSearch);
        } catch (Exception e) {
            
        }
        double toSearchDouble = 0;
        try {
            toSearchDouble = Double.parseDouble(toSearch);
        } catch (Exception e) {
            
        }
        long phoneToSearch = 0;
        try {
            phoneToSearch = Long.parseLong(toSearch);
        } catch (Exception e) {
            
        }
        
        System.out.println(toSearchDouble);
        
        List<Person> results;
        
        Pageable firstPageWithTwoElements = PageRequest.of(page, quan, Sort.by("firstName").descending());
        
        results = personRepo.searchByFirstNameContainingAllIgnoreCase(toSearch, firstPageWithTwoElements);
        results.addAll(personRepo.searchBySurNameContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByAge(ageToSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByDescriptionContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        
        results.addAll(personRepo.searchByCollegeContainingAllIgnoreCase(toSearch, firstPageWithTwoElements));
        results.addAll(personRepo.searchBySchoolContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByEmployerContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByOccupationContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByHighestEducationQualificationContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByEyecolorContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByWeight(toSearchDouble,firstPageWithTwoElements));
        results.addAll(personRepo.searchByHeight(toSearchDouble,firstPageWithTwoElements));
        results.addAll(personRepo.searchByPPSnumberIgnoreCase(toSearch,firstPageWithTwoElements));
        //drivers and provisional
        results.addAll(personRepo.searchByBankIBANAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByPhoneNumber(phoneToSearch,firstPageWithTwoElements));
        //gender
        results.addAll(personRepo.searchByEmailAddressIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByWebsiteAddressContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        results.addAll(personRepo.searchByHomeAddressContainingAllIgnoreCase(toSearch,firstPageWithTwoElements));
        
        results = removeDuplicatePersons(results);
        
        return results;
    }

    List<Person> removeDuplicatePersons(List<Person> toRemove){
        List<Person> result = toRemove;
        
         List<Person> listWithoutDuplicates = new ArrayList<>(
                  new HashSet<Person>(result));
         
        return listWithoutDuplicates;
    }

}

As you will see there is a hardcoded Sort object with firstName and descending. Whenever I call this function, it returns a randomly sorted data. The sort doesn't work. I went to the effort to hardcode it to eliminate the chance of parameter data corruption but even the hardcode doesn't work.

toSearch is a String search query. Page and quan (quantity) are for pagination. Pagination works but sort doesn't. Any help is appreciated, if you need me to explain the code more, add a comment.

There's a controller class also, as you would probably imagine. I could add that code too but it doesn't directly affect the logic of this code. The controller calls the service function and returns it as JSON to a web app. I have debugged both in Postman, by requesting the REST controller function which invokes the service function. It returns data to Postman as JSON and I did the same in my implementing web app, but the data is not sorted.

You'll notice the 4 annotations on the Person class model. Entity is for persistence. NoArgsConstructor and Getter and Setter are part of Lombok, a package that allows you to omit getters, setters, constructors and they are added at compile time.

The problem is not with sort. The problem is with the way you perform your search. You call 18 different searches and merge them. Every single search is sorted, but it does not guarantee that your results list is sorted.

Example:

  1. The first search returns: {"Betty", "Adam"} (properly sorted by first name descending)
  2. The second search returns: {"Zack", "Fiona"} (properly sorted by first name descending)

But the result: {"Betty", "Adam", "Zack", "Fiona"} looks like a random order.

You can always sort on the Java side, but it's not recommended due to performance issues on large lists.

To fix the problem, you need to search with one query. You can use a spring boot query by specification to achieve that, more information you can find here .

You are probably looking for the JPA Criteria API. You can build your query using the JPA Specification API which has 2 clear advantages over what you are doing:

  1. Can do the query in one vs many queries (more performant).
  2. Is more simple to change and adapt once built (if you design you classes/patterns well).

A quick example for you here:

@Repository
public interface PersonRepo extends JpaRepository<Person,Long>, JpaSpecificationExecutor<Person> {}

And here is a quick component I have written. You can see where it uses the JPA Specification API to create the SQL and then runs it using the Repo.

@Component
public class PersonSearcher {

    @Autowired
    private PersonRepo personRepo;

    /*
        Would be better taking a "Form"/Object with your search criteria.
     */
    public Page<Person> search(String name, Integer ageMin, Integer ageMax, Pageable pageable) {

        //Get "all"
        Specification<Person> personSpecification = Specification.not(null);

        //Create "Predicates" (like the where clauses).
        if (name != null) {
            personSpecification = personSpecification.and(new MyPersonSpec("firstName", name, "like"));
        }
        if (ageMin != null) {
            personSpecification = personSpecification.and(new MyPersonSpec("age", ageMin, "gt"));
        }

        if (ageMax != null) {
            personSpecification = personSpecification.and(new MyPersonSpec("age", ageMax, "lt"));
        }


        //Run query using Repo. Spring paging still works.
        return personRepo.findAll(personSpecification, pageable);

    }

    private static class MyPersonSpec implements Specification<Person> {

        private final String field;
        private final Object value;
        private final String operation;

        private MyPersonSpec(String field, Object value, String operation) {
            this.field = field;
            this.value = value;
            this.operation = operation;
        }

        @Override
        public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
            switch (operation) {
                case "like":
                    return criteriaBuilder.like(root.get(field), "%" + value.toString().toLowerCase() + "%");
                case "equal":
                    return criteriaBuilder.equal(root.get(field), value);
                case "gt":
                    return criteriaBuilder.greaterThan(root.get(field), (int) value);
                case "lt":
                    return criteriaBuilder.lessThan(root.get(field), (int) value);

                default:
                    throw new RuntimeException("Unexpected `op`.");
            }
        }
    }
}

Here is a quick test to ensure it compiles...you'll want to do some proper assertions and maybe @DataJpa test instead of @SpringBootTest...

@DataJpaTest
@ActiveProfiles("tc")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Log4j2
@Transactional
@Sql(statements = {
        "INSERT INTO person (USER_ID,first_name,age,drivers_licence,provisional_licence) values(1,'Bob',31,true,false)",
        "INSERT INTO person (USER_ID,first_name,age,drivers_licence,provisional_licence) values(2,'Alice',56,true,false)",
        "INSERT INTO person (USER_ID,first_name,age,drivers_licence,provisional_licence) values(3,'Charlie',18,false,false)",
        "INSERT INTO person (USER_ID,first_name,age,drivers_licence,provisional_licence) values(4,'Dave',72,true,true)",
        "INSERT INTO person (USER_ID,first_name,age,drivers_licence,provisional_licence) values(5,'Emma',21,false,true)"
})
@Import(PersonSearcher.class)
class PersonSearcherTest {

    @Autowired
    private PersonSearcher personSearcher;

    @Test
    void search() {

        /*
         * Test 1 : Finds Bob using name.
         */

        //Run the searcher to find "Bob".
        final Page<Person> search = personSearcher.search("bob", null, null,
                PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "emailAddress"))
        );
        log.info(search);

        //Assert Returns Bob (ID 1)
        assertEquals(1L, search.getContent().get(0).getId());

        /*
         * Test 2: Name and age range with an order by age in Paging.
         */

        //Search with any name with an A/a in it between ages 20->99.
        final Page<Person> search2 = personSearcher.search("a", 20, 100,
                PageRequest.of(0, 5, Sort.Direction.ASC, "age"));

        log.info(search2.getContent());
        //Assert fetches only ones with an `a` or `A` and should have age >20 and <100.
        assertTrue(search2
                .stream()
                .allMatch(it ->
                        it.getFirstName().toLowerCase().contains("a")
                                && it.getAge() > 20
                                && it.getAge() < 100
                ));

        //Assert they are Alice,Dave and Emma (NOT Charlie as <20 age) and in age order from youngest to oldest...
        Assertions.assertEquals(Arrays.asList("Emma", "Alice", "Dave"),
                search2.get().map(Person::getFirstName).collect(Collectors.toList()));


        /*
         * Test 3 : With null values gets all back with order by firstName ASC
         */

        final Page<Person> allSearch = personSearcher.search(null, null, null,
                PageRequest.of(0, 10, Sort.Direction.ASC, "firstName"));

        //Assert all back in name order.
        assertEquals(Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Emma"),
                allSearch.get().map(Person::getFirstName).collect(Collectors.toList()));
    }
}

And the SQL the specification produced and was run in logs:

08 Jan 2021 23:24:19,840 [DEBUG] --- o.h.SQL                        : 
    select
        person0_.user_id as user_id1_18_,
        person0_.dob as dob2_18_,
        person0_.ppsnumber as ppsnumbe3_18_,
        person0_.age as age4_18_,
        person0_.bankiban as bankiban5_18_,
        person0_.college as college6_18_,
        person0_.description as descript7_18_,
        person0_.drivers_licence as drivers_8_18_,
        person0_.email_address as email_ad9_18_,
        person0_.employer as employe10_18_,
        person0_.eyecolor as eyecolo11_18_,
        person0_.first_name as first_n12_18_,
        person0_.gender as gender13_18_,
        person0_.height as height14_18_,
        person0_.highest_education_qualification as highest15_18_,
        person0_.home_address as home_ad16_18_,
        person0_.occupation as occupat17_18_,
        person0_.phone_number as phone_n18_18_,
        person0_.provisional_licence as provisi19_18_,
        person0_.school as school20_18_,
        person0_.sur_name as sur_nam21_18_,
        person0_.website_address as website22_18_,
        person0_.weight as weight23_18_ 
    from
        person person0_ 
    where
        person0_.age<20 
        and person0_.age>1 
        and (
            person0_.first_name like ?
        ) 
    order by
        person0_.email_address desc limit ?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM