简体   繁体   English

可分页排序在 Spring 数据 JPA、Spring 框架中不起作用

[英]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.这是 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.服务 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.正如您将看到的,有一个硬编码的排序 object,带有名字和降序。 Whenever I call this function, it returns a randomly sorted data.每当我调用这个 function 时,它都会返回一个随机排序的数据。 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. toSearch是一个字符串搜索查询。 Page and quan (quantity) are for pagination. Pagequan (数量)用于分页。 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.正如您可能想象的那样,还有一个 controller class 。 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. controller 调用服务 function 并将其作为 JSON 返回给 Z2567A5EC3705EB7AC2C1E96 应用程序。 I have debugged both in Postman, by requesting the REST controller function which invokes the service function. 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.它以 JSON 的形式将数据返回给 Postman,我在实现 web 应用程序时也做了同样的事情,但数据没有排序。

You'll notice the 4 annotations on the Person class model.您会注意到 Person class model 上的 4 个注释。 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. NoArgsConstructor 和 Getter 和 Setter 是 Lombok 的一部分,package 允许您省略 getter、setter、构造函数,它们是在编译时添加的。

The problem is not with sort.问题不在于排序。 The problem is with the way you perform your search.问题在于您执行搜索的方式。 You call 18 different searches and merge them.您调用 18 个不同的搜索并将它们合并。 Every single search is sorted, but it does not guarantee that your results list is sorted.每个搜索都已排序,但不能保证您的results列表已排序。

Example:例子:

  1. The first search returns: {"Betty", "Adam"} (properly sorted by first name descending)第一次搜索返回: {"Betty", "Adam"} (按名字降序正确排序)
  2. The second search returns: {"Zack", "Fiona"} (properly sorted by first name descending)第二次搜索返回: {"Zack", "Fiona"} (按名字降序正确排序)

But the result: {"Betty", "Adam", "Zack", "Fiona"} looks like a random order.但结果: {"Betty", "Adam", "Zack", "Fiona"}看起来像一个随机顺序。

You can always sort on the Java side, but it's not recommended due to performance issues on large lists.您始终可以在 Java 端进行排序,但由于大型列表的性能问题,不建议这样做。

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 .您可以按规范使用 spring 引导查询来实现这一点,您可以在此处找到更多信息。

You are probably looking for the JPA Criteria API.您可能正在寻找 JPA 标准 API。 You can build your query using the JPA Specification API which has 2 clear advantages over what you are doing:您可以使用 JPA 规范 API 构建您的查询,它比您正在做的事情有两个明显的优势:

  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.您可以看到它在哪里使用 JPA 规范 API 创建 SQL,然后使用 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...这是一个快速测试以确保它可以编译...您需要做一些正确的断言,也许是@DataJpa 测试而不是@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: SQL 规范产生并在日志中运行:

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 ?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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