【问题标题】:Spring Data optional parameter in query method查询方法中的 Spring Data 可选参数
【发布时间】:2015-12-20 03:18:27
【问题描述】:

我想在存储库层编写一些查询方法。此方法必须忽略空参数。例如:

List<Foo> findByBarAndGoo(Bar barParam, @optional Goo gooParam);

此方法必须在此条件下返回 Foo:

bar == barParam && goo == gooParam;

如果 gooParam 不为空。如果 gooParam 为空,则条件更改为:

bar == barParam;

有什么解决办法吗?有人可以帮我吗?

【问题讨论】:

  • List findByBarAndGoo(Bar bar, @optional Goo goo){if(bar==null || goo==null){throw new IllegalArgumentException("参数不能为空"); } }
  • 我的意思是在可选参数中,如果此参数为空,则查询方法忽略此参数。谢谢。
  • 你可以使用stackoverflow.com/a/63776549/5448746中提到的SpEL

标签: spring hibernate spring-data spring-data-jpa jpql


【解决方案1】:

我不相信您可以通过查询定义的方法名称方法来做到这一点。来自文档(reference):

虽然从方法名派生出一个查询是相当的 方便,一个人可能会面临这样的情况,其中任一方法 名称解析器不支持要使用的关键字或方法 名字会变得不必要地难看。所以你可以使用 JPA 命名 通过命名约定进行查询(请参阅 Using JPA NamedQueries for 更多信息)或者更确切地说使用 @Query 注释您的查询方法

我想你这里也有这种情况,所以下面的答案使用@Query注解方式,几乎和方法名方式(reference)一样方便。

    @Query("select foo from Foo foo where foo.bar = :bar and "
        + "(:goo is null or foo.goo = :goo)")
    public List<Foo> findByBarAndOptionalGoo(
        @Param("bar") Bar bar, 
        @Param("goo") Goo goo);

【讨论】:

    【解决方案2】:

    回答太晚了。不确定 BarGoo 之间的关系。检查示例是否可以帮助您。

    它对我有用。我有类似的情况,实体 User 有一组属性,并且有 findAll 方法可以根据属性搜索用户(这是可选的)。

    例子,

      Class User{
        String firstName;
        String lastName;
        String id;
      }
    
      Class UserService{
         // All are optional
         List<User> findBy(String firstName, String lastName, String id){
            User u = new User();
            u.setFirstName(firstName);
            u.setLastName(lastName);
            u.setId(id);
    
            userRepository.findAll(Example.of(user));
            // userRepository is a JpaRepository class
         }
      }
    

    【讨论】:

    • 我在那里发布了一个与我的代码类似的问题,你能看看另一篇帖子,如果我做错了什么,请告诉我?我的代码遵循这个例子。 stackoverflow.com/questions/54955376/… 谢谢!
    • @DenissM。你能分享你的代码吗?仅供参考,我在我的项目中使用它,这里是它接受的答案。不知道为什么-1。
    • 这太棒了!可以通过这种方式轻松概括复杂的查询。
    【解决方案3】:

    作为@chaserb 答案的补充,我个人会将参数添加为Java8 Optional 类型,以使其在方法签名中明确表示作为可选过滤器的语义。

    @Query("select foo from Foo foo where foo.bar = :bar and "
       + "(:goo is null or foo.goo = :goo)")
    public List<Foo> findByBarAndOptionalGoo(
         @Param("bar") Bar bar, 
         @Param("goo") Optional<Goo> goo);
    

    【讨论】:

    • 我必须添加nativeQuery = true 作为第二个@Query 参数,否则在运行应用程序时出现错误:IllegalArgumentException: Validation failed for query
    • Optional 作为参数传递不是一个好主意:stackoverflow.com/questions/31922866/…
    • 我在尝试您的建议时收到此错误:org.postgresql.util.PSQLException: ERROR: could not determine data type of parameter $1 知道如何解决此问题吗?
    【解决方案4】:

    你可以使用JpaSpecificationExecutor //import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

    第 1 步:在您的 JPA 存储库中实现 JpaSpecificationExecutor

    例如:

    public interface TicketRepo extends JpaRepository<Ticket, Long>, JpaSpecificationExecutor<Ticket> {
    

    第 2 步现在要根据可选参数获取票证,您可以使用 CriteriaBuilder 构建规范查询

    例如:

    public Specification<Ticket> getTicketQuery(Integer domainId, Calendar startDate, Calendar endDate, Integer gameId, Integer drawId) {
        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
    
            predicates.add(criteriaBuilder.equal(root.get("domainId"), domainId));
            predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createdAt"), startDate));
            predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("createdAt"), endDate));
    
            if (gameId != null) {
                predicates.add(criteriaBuilder.equal(root.get("gameId"), gameId));
            }
    
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
    

    第 3 步:将 Specification 实例传递给 jpaRepo.findAll(specification),它将返回您的实体对象列表(在运行示例中为 Tickets)

    ticketRepo.findAll(specification); // Pass output of function in step 2 to findAll
    

    【讨论】:

      【解决方案5】:

      您只需几行代码就可以自己编写代码:

      List<Foo> findByBarAndOptionalGoo(Bar bar, Goo goo) {
         return (goo == null) ? this.findByBar(bar) : this.findByBarAndGoo(bar, goo);
      }
      

      否则,我不知道 Spring-Data 是否支持这个开箱即用。

      【讨论】:

      • 适用于 2 个过滤器,但如果您有 4 个可选过滤器则不可能。
      【解决方案6】:

      已经有很多很棒的答案,但我使用@Pankaj Garg 的答案(使用 Spring Specification API)专门实现了这个。我在回答中添加了一些用例

      • 4 个参数,可能为空,也可能不为空。
      • 来自存储库的分页响应。
      • 按嵌套对象中的字段过滤。
      • 按特定字段排序。

      首先我创建了几个实体,特别是TicketMovieCustomer。这里没什么特别的:

      import lombok.AllArgsConstructor;
      import lombok.Builder;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      
      import javax.persistence.*;
      import javax.validation.constraints.NotNull;
      import javax.validation.constraints.Size;
      import java.io.Serializable;
      import java.math.BigDecimal;
      import java.util.Date;
      import java.util.UUID;
      
      @Entity
      @Table(name = "ticket", schema = "public")
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      @Builder(toBuilder = true)
      public class Ticket implements Serializable  {
      
          @Id
          @Basic(optional = false)
          @NotNull
          @Column(name = "id", nullable = false)
          private UUID id;
      
          @JoinColumn(name = "movie_id", referencedColumnName = "id", nullable = false)
          @ManyToOne(fetch = FetchType.EAGER)
          private Movie movie;
      
          @JoinColumn(name = "customer_id", referencedColumnName = "id", nullable = false)
          @ManyToOne(fetch = FetchType.EAGER)
          private Customer customer;
      
          @Column(name = "booking_date")
          @Temporal(TemporalType.TIMESTAMP)
          private Date bookingDate;
      }
      

      电影:

      import lombok.AllArgsConstructor;
      import lombok.Builder;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      
      import javax.persistence.*;
      import javax.validation.constraints.NotNull;
      import javax.validation.constraints.Size;
      import java.io.Serializable;
      
      @Entity
      @Table(name = "movie", schema = "public")
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      @Builder(toBuilder = true)
      public class Movie implements Serializable {
      
          @Id
          @Basic(optional = false)
          @NotNull
          @Column(name = "id", nullable = false)
          private UUID id;
      
          @Basic(optional = false)
          @NotNull
          @Size(max = 100)
          @Column(name = "movie_name", nullable = false, length = 100)
          private String movieName;
      }
      

      客户:

      import lombok.AllArgsConstructor;
      import lombok.Builder;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      
      import javax.persistence.*;
      import javax.validation.constraints.NotNull;
      import javax.validation.constraints.Size;
      import java.io.Serializable;
      
      @Entity
      @Table(name = "customer", schema = "public")
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      @Builder(toBuilder = true)
      public class Customer implements Serializable {
      
          @Id
          @Basic(optional = false)
          @NotNull
          @Column(name = "id", nullable = false)
          private UUID id;
      
          @Basic(optional = false)
          @NotNull
          @Size(max = 100)
          @Column(name = "full_name", nullable = false, length = 100)
          private String fullName;
      }
      

      然后我创建一个类,其中包含我希望过滤的参数的字段:

      import lombok.AllArgsConstructor;
      import lombok.Data;
      
      import java.util.Date;
      import java.util.UUID;
      
      @Data
      @AllArgsConstructor
      public class TicketFilterParam {
          private UUID movieId;
          private UUID customerId;
          private Date start;
          private Date end;
      }
      

      接下来我创建一个类来根据过滤器参数生成Specification。请注意访问嵌套对象的方式,以及将排序添加到查询的方式。

      import org.springframework.data.jpa.domain.Specification;
      
      import javax.persistence.criteria.Predicate;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.UUID;
      
      public class TicketSpecifications {
          public static Specification<Ticket> getFilteredTickets(TicketFilterParam params) {
              return (root, criteriaQuery, criteriaBuilder) -> {
                  List<Predicate> predicates = new ArrayList<>();
      
                  if (params.getMovieId() != null) {
                      predicates.add(criteriaBuilder.equal(root.get("movie").<UUID> get("id"), params.getMarketerId()));
                  }
      
                  if (params.getCustomerId() != null) {
                      predicates.add(criteriaBuilder.equal(root.get("customer").<UUID> get("id"), params.getDepotId()));
                  }
      
                  if (params.getStart() != null && params.getEnd() != null) {
                      predicates.add(criteriaBuilder.between(root.get("bookingDate"), params.getStart(), params.getEnd()));
                  }
      
                  criteriaQuery.orderBy(criteriaBuilder.desc(root.get("bookingDate")));
      
                  return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
              };
          }
      }
      

      接下来我定义存储库接口。这不仅有JpaRepository,还有JpaSpecificationExecutor

      import org.springframework.data.jpa.repository.JpaRepository;
      import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
      import org.springframework.stereotype.Repository;
      
      @Repository
      public interface TicketRepository extends JpaRepository<Ticket, UUID>, JpaSpecificationExecutor<Ticket> {
      }
      

      最后,在某个服务类中,我得到如下结果:

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.domain.Page;
      import org.springframework.data.domain.PageRequest;
      import org.springframework.data.jpa.domain.Specification;
      import org.springframework.stereotype.Service;
      
      @Service
      public class TicketService {
          @Autowired
          private TicketRepository ticketRepository;
      
          public Page<Ticket> getTickets(TicketFilterParam params, PageRequest pageRequest) {
              Specification<Ticket> specification = TicketSpecifications.getFilteredTickets(params);
              return ticketRepository.findAll(specification, pageRequest);
          }
      }
      

      PageRequestTicketFilterParam 可能会从休息端点上的某些参数和值中获得。

      【讨论】:

        【解决方案7】:

        回答为时已晚,但对于正在寻找解决方案但有以下更简单方法的人来说,我遇到了同样的问题,终于可以找到这个看起来比其他解决方案非常简单和高效的解决方案对我来说:

        我的控制器类:

        @RestController
        @RequestMapping("/order")
        public class OrderController {
        
            private final IOrderService service;
        
            public OrderController(IOrderService service) {
                this.service = service;
            }
        
            @RequestMapping(value = "/{username}/", method = RequestMethod.GET)
            public ResponseEntity<ListResponse<UserOrdersResponse>> getUserOrders(
                    @RequestHeader Map<String, String> requestHeaders,
                    @RequestParam(required=false) Long id,
                    @RequestParam(required=false) Long flags,
                    @RequestParam(required=true) Long offset,
                    @RequestParam(required=true) Long length) {
                // Return successful response
                return new ResponseEntity<>(service.getUserOrders(requestDTO), HttpStatus.OK);
            }
        }
        

        如您所见,我有 Username@PathVariablelengthoffset 这是我的必需参数,但我接受 idflags 用于过滤搜索结果,所以它们是我的可选参数,对于调用 REST 服务不是必需的。

        我的存储库界面:

        @Query("select new com.ada.bourse.wealth.services.models.response.UserOrdersResponse(FIELDS ARE DELETED TO BECOME MORE READABLE)" +
                " from User u join Orders o on u.id = o.user.id where u.userName = :username" +
                " and (:orderId is null or o.id = :orderId) and (:flag is null or o.flags = :flag)")
        Page<UserOrdersResponse> findUsersOrders(String username, Long orderId, Long flag, Pageable page);
        

        就是这样,你可以看到我用(:orderId is null or o.id = :orderId)(:flag is null or o.flags = :flag)检查了我的可选参数,我认为需要强调的是我用is null检查了我的参数条件不是我的列数据,所以如果客户端为我发送Idflags 参数,我将使用它们过滤结果,否则我只需使用username 查询,这是我的@PathVariable

        【讨论】:

          猜你喜欢
          • 2012-07-21
          • 1970-01-01
          • 2019-03-16
          • 2018-08-16
          • 2020-01-08
          • 1970-01-01
          • 1970-01-01
          • 2014-09-22
          相关资源
          最近更新 更多