简体   繁体   English

使用spring-data-jpa和spring-mvc过滤数据库行

[英]Filtering database rows with spring-data-jpa and spring-mvc

I have a spring-mvc project that is using spring-data-jpa for data access. 我有一个spring-mvc项目,正在使用spring-data-jpa进行数据访问。 I have a domain object called Travel which I want to allow the end-user to apply a number of filters to it. 我有一个称为Travel的域对象,我希望允许最终用户对其应用一些过滤器。

For that, I've implemented the following controller: 为此,我实现了以下控制器:

@Autowired
private TravelRepository travelRep;

@RequestMapping("/search")  
public ModelAndView search(
        @RequestParam(required= false, defaultValue="") String lastName, 
        Pageable pageable) {  
    ModelAndView mav = new ModelAndView("travels/list");  
    Page<Travel> travels  = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
    PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
    mav.addObject("page", page);
    mav.addObject("lastName", lastName);
    return mav;
}

This works fine: The user has a form with a lastName input box which can be used to filter the Travels. 效果很好:用户有一个带有lastName输入框的表单,该表单可用于过滤Travels。

Beyond lastName, my Travel domain object has a lot more attributes by which I'd like to filter. 除了lastName之外,我的Travel域对象还有很多我想用来过滤的属性。 I think that if these attributes were all strings then I could add them as @RequestParam s and add a spring-data-jpa method to query by these. 我认为,如果这些属性都是字符串,那么我可以将它们添加为@RequestParam并添加spring-data-jpa方法以通过这些方法进行查询。 For instance I'd add a method findByLastNameLikeAndFirstNameLikeAndShipNameLike . 例如,我将添加一个方法findByLastNameLikeAndFirstNameLikeAndShipNameLike

However, I don't know how should I do it when I need to filter for foreign keys. 但是,当需要过滤外键时,我不知道该怎么办。 So my Travel has a period attribute that is a foreign key to the Period domain object, which I need to have it as a dropdown for the user to select the Period . 因此,我的Travel拥有一个period属性,该属性是Period域对象的外键,我需要将其作为下拉菜单,以便用户选择Period

What I want to do is when the period is null I want to retrieve all travels filtered by the lastName and when the period is not null I want to retrieve all travels for this period filtered by the lastName. 我想做的是,当时间段为null时,我要检索由lastName过滤的所有行程;当时间段不为null时,我要检索由lastName过滤的该段时间的所有行程。

I know that this can be done if I implement two methods in my repository and use an if to my controller: 我知道,如果我在存储库中实现两个方法并对控制器使用if ,就可以做到这一点:

public ModelAndView search(
       @RequestParam(required= false, defaultValue="") String lastName,
       @RequestParam(required= false, defaultValue=null) Period period, 
       Pageable pageable) {  
  ModelAndView mav = new ModelAndView("travels/list");  
  Page travels = null;
  if(period==null) {
    travels  = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
  } else {
    travels  = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable);
  }
  mav.addObject("page", page);
  mav.addObject("period", period);
  mav.addObject("lastName", lastName);
  return mav;
}

Is there a way to do this without using the if ? 没有不使用if吗? My Travel has not only the period but also other attributes that need to be filtered using dropdowns !! 我的旅行不仅具有期间,还具有需要使用下拉列表过滤的其他属性! As you can understand, the complexity would be exponentially increased when I need to use more dropdowns because all the combinations'd need to be considered :( 如您所知,当我需要使用更多的下拉菜单时,复杂度将成倍增加,因为需要考虑所有组合:(

Update 03/12/13 : Continuing from M. Deinum's excelent answer, and after actually implementing it, I'd like to provide some comments for completeness of the question/asnwer: 2013年3月12日更新接替Deinum先生的出色回答,在实际实施之后,我想提出一些评论,以确保问题/答案的完整性:

  1. Instead of implementing JpaSpecificationExecutor you should implement JpaSpecificationExecutor<Travel> to avoid type check warnings. 代替实现JpaSpecificationExecutor您应该实现JpaSpecificationExecutor<Travel>以避免类型检查警告。

  2. Please take a look at kostja's excellent answer to this question Really dynamic JPA CriteriaBuilder since you will need to implement this if you want to have correct filters. 请看一下kostja对这个问题的出色回答。 真正的动态JPA CriteriaBuilder,因为如果您想拥有正确的过滤器, 需要实现这一点。

  3. The best documentation I was able to find for the Criteria API was http://www.ibm.com/developerworks/library/j-typesafejpa/ . 我可以找到的有关Criteria API的最佳文档是http://www.ibm.com/developerworks/library/j-typesafejpa/ This is a rather long read but I totally recommend it - after reading it most of my questions for Root and CriteriaBuilder were answered :) 这是一篇相当长的读物,但我完全推荐它-阅读后,我对Root和CriteriaBuilder的大部分问题都得到了回答:)

  4. Reusing the Travel object was not possible because it contained various other objects (who also contained other objects) which I needed to search for using Like - instead I used a TravelSearch object that contained the fields I needed to search for. 重用Travel对象是不可能的,因为它包含我需要使用Like搜索的其他各种对象(也包含其他对象)-相反,我使用了TravelSearch对象,其中包含需要搜索的字段。

Update 10/05/15 : As per @priyank's request, here's how I implemented the TravelSearch object: 2015年10月5日更新 :按照@priyank的要求,这是我实现TravelSearch对象的方式:

public class TravelSearch {
    private String lastName;
    private School school;
    private Period period;
    private String companyName;
    private TravelTypeEnum travelType;
    private TravelStatusEnum travelStatus;
    // Setters + Getters
}

This object was used by TravelSpecification (most of the code is domain specific but I'm leaving it there as an example): TravelSpecification使用了此对象(大多数代码是特定于域的,但我将其留在此处作为示例):

public class TravelSpecification implements Specification<Travel> {
    private TravelSearch criteria;


    public TravelSpecification(TravelSearch ts) {
        criteria= ts;
    }

    @Override
    public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query, 
            CriteriaBuilder cb) {
        Join<Travel, Candidacy> o = root.join(Travel_.candidacy);

        Path<Candidacy> candidacy = root.get(Travel_.candidacy);
        Path<Student> student = candidacy.get(Candidacy_.student);
        Path<String> lastName = student.get(Student_.lastName);
        Path<School> school = student.get(Student_.school);

        Path<Period> period = candidacy.get(Candidacy_.period);
        Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
        Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);

        Path<Company> company = root.get(Travel_.company);
        Path<String> companyName = company.get(Company_.name);

        final List<Predicate> predicates = new ArrayList<Predicate>();
        if(criteria.getSchool()!=null) {
            predicates.add(cb.equal(school, criteria.getSchool()));
        }
        if(criteria.getCompanyName()!=null) {
            predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%"));
        }
        if(criteria.getPeriod()!=null) {
            predicates.add(cb.equal(period, criteria.getPeriod()));
        }
        if(criteria.getTravelStatus()!=null) {
            predicates.add(cb.equal(travelStatus, criteria.getTravelStatus()));
        }
        if(criteria.getTravelType()!=null) {
            predicates.add(cb.equal(travelType, criteria.getTravelType()));
        }
        if(criteria.getLastName()!=null ) {
            predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%"));
        }
        return cb.and(predicates.toArray(new Predicate[predicates.size()]));

    }
}

Finally, here's my search method: 最后,这是我的搜索方法:

@RequestMapping("/search")  
public ModelAndView search(
        @ModelAttribute TravelSearch travelSearch,
        Pageable pageable) {  
    ModelAndView mav = new ModelAndView("travels/list");  

    TravelSpecification tspec = new TravelSpecification(travelSearch);

    Page<Travel> travels  = travelRep.findAll(tspec, pageable);

    PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");

    mav.addObject(travelSearch);

    mav.addObject("page", page);
    mav.addObject("schools", schoolRep.findAll() );
    mav.addObject("periods", periodRep.findAll() );
    mav.addObject("travelTypes", TravelTypeEnum.values());
    mav.addObject("travelStatuses", TravelStatusEnum.values());
    return mav;
}

Hope I helped! 希望我能帮上忙!

For starters you should stop using @RequestParam and put all your search fields in an object (maybe reuse the Travel object for that). 对于初学者,您应该停止使用@RequestParam并将所有搜索字段放在一个对象中(也许为此使用Travel对象)。 Then you have 2 options which you could use to dynamically build a query 然后,您可以使用2个选项来动态构建查询

  1. Use the JpaSpecificationExecutor and write a Specification 使用JpaSpecificationExecutor并编写Specification
  2. Use the QueryDslPredicateExecutor and use QueryDSL to write a predicate. 使用QueryDslPredicateExecutor并使用QueryDSL编写谓词。

Using JpaSpecificationExecutor 使用JpaSpecificationExecutor

First add the JpaSpecificationExecutor to your TravelRepository this will give you a findAll(Specification) method and you can remove your custom finder methods. 首先,将JpaSpecificationExecutor添加到TravelRepository这将为您提供findAll(Specification)方法,并且您可以删除自定义查找器方法。

public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}

Then you can create a method in your repository which uses a Specification which basically builds the query. 然后,您可以在存储库中创建一个使用Specification的方法,该Specification基本上可以构建查询。 See the Spring Data JPA documentation for this. 请参阅Spring Data JPA 文档

The only thing you need to do is create a class which implements Specification and which builds the query based on the fields which are available. 您唯一需要做的就是创建一个实现Specification并基于可用字段构建查询的类。 The query is build using the JPA Criteria API link. 使用JPA Criteria API链接构建查询。

public class TravelSpecification implements Specification<Travel> {

    private final Travel criteria;

    public TravelSpecification(Travel criteria) {
        this.criteria=criteria;
    }

    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        // create query/predicate here.
    }
}

And finally you need to modify your controller to use the new findAll method (I took the liberty to clean it up a little). 最后,您需要修改您的控制器以使用新的findAll方法(我冒昧地将其清理了一点)。

@RequestMapping("/search")  
public String search(@ModelAttribute Travel search, Pageable pageable, Model model) {  
Specification<Travel> spec = new TravelSpecification(search);
    Page<Travel> travels  = travelRep.findAll(spec, pageable);
    model.addObject("page", new PageWrapper(travels, "/search"));
    return "travels/list";
}

Using QueryDslPredicateExecutor 使用QueryDslPredicateExecutor

First add the QueryDslPredicateExecutor to your TravelRepository this will give you a findAll(Predicate) method and you can remove your custom finder methods. 首先将QueryDslPredicateExecutor添加到TravelRepository这将为您提供findAll(Predicate)方法,并且您可以删除自定义查找器方法。

public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}

Next you would implement a service method which would use the Travel object to build a predicate using QueryDSL. 接下来,您将实现一种服务方法,该方法将使用Travel对象使用QueryDSL构建谓词。

@Service
@Transactional
public class TravelService {

    private final TravelRepository travels;

    public TravelService(TravelRepository travels) {
        this.travels=travels;
    }

    public Iterable<Travel> search(Travel criteria) {

        BooleanExpression predicate = QTravel.travel...
        return travels.findAll(predicate);
    }
}

See also this bog post . 另请参阅此沼泽帖子

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

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