Spring 查询方法中的数据可选参数
Spring Data optional parameter in query method
我想在repository层写一些查询方法。此方法必须忽略空参数。例如:
List<Foo> findByBarAndGoo(Bar barParam, @optional Goo gooParam);
这个方法必须是 return Foo 这个条件:
bar == barParam && goo == gooParam;
如果 gooParam 不为空。如果 gooParam 为空,则条件更改为:
bar == barParam;
有什么解决办法吗?有人可以帮助我吗?
您只需几行就可以自己编写代码:
List<Foo> findByBarAndOptionalGoo(Bar bar, Goo goo) {
return (goo == null) ? this.findByBar(bar) : this.findByBarAndGoo(bar, goo);
}
不然不知道Spring-Data开箱即用。
我认为您无法使用查询定义的方法名称方法来做到这一点。来自文档 (reference):
Although getting a query derived from the method name is quite
convenient, one might face the situation in which either the method
name parser does not support the keyword one wants to use or the method
name would get unnecessarily ugly. So you can either use JPA named
queries through a naming convention (see Using JPA NamedQueries for
more information) or rather annotate your query method with @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);
补充@chaserb 的答案,我个人会将参数添加为 Java8 可选类型,以使其在方法签名中显式显示作为可选过滤器的语义。
@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);
来不及回答。不确定 Bar 和 Goo 之间的关系。检查 Example 是否可以帮助您。
它对我有用。我有类似的情况,实体 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
}
}
你可以使用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),它将 return 您的实体对象列表(运行 示例中的 Tickets)
ticketRepo.findAll(specification); // Pass output of function in step 2 to findAll
现在回答也太晚了,但对于任何正在寻找解决方案但有更简单方法的人来说,我遇到了同样的问题,最终找到了这个看起来非常简单有效的解决方案其他给我:
我的控制器Class:
@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
作为 @PathVariable
和 length
和 offset
是我需要的参数,但我接受 id
和flags
用于过滤搜索结果,所以它们是我的可选参数,调用 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
条件 不是我的列数据 ,所以如果客户端为我发送 Id
和 flags
参数,我将用它们过滤结果否则我只是用 username
查询这是我的 @PathVariable
.
已经有很多很棒的答案,但我使用@Pankaj Garg 的答案(使用 Spring 规范 API)专门实现了这个。我在回答中添加了一些用例
- 4 个参数可能为空也可能不为空。
- 来自存储库的分页响应。
- 正在按嵌套对象中的字段过滤。
- 按特定字段排序。
首先,我创建了几个实体,特别是 Ticket
、Movie
和 Customer
。这里没什么特别的:
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;
}
然后我创建一个 class,其中包含我希望过滤的参数字段:
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;
}
接下来我创建一个 class 以根据过滤器参数生成一个 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> {
}
最后,在某些服务 class 中,我得到这样的结果:
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);
}
}
PageRequest
和 TicketFilterParam
可能会从休息端点上的一些参数和值中获得。
我想在repository层写一些查询方法。此方法必须忽略空参数。例如:
List<Foo> findByBarAndGoo(Bar barParam, @optional Goo gooParam);
这个方法必须是 return Foo 这个条件:
bar == barParam && goo == gooParam;
如果 gooParam 不为空。如果 gooParam 为空,则条件更改为:
bar == barParam;
有什么解决办法吗?有人可以帮助我吗?
您只需几行就可以自己编写代码:
List<Foo> findByBarAndOptionalGoo(Bar bar, Goo goo) {
return (goo == null) ? this.findByBar(bar) : this.findByBarAndGoo(bar, goo);
}
不然不知道Spring-Data开箱即用。
我认为您无法使用查询定义的方法名称方法来做到这一点。来自文档 (reference):
Although getting a query derived from the method name is quite convenient, one might face the situation in which either the method name parser does not support the keyword one wants to use or the method name would get unnecessarily ugly. So you can either use JPA named queries through a naming convention (see Using JPA NamedQueries for more information) or rather annotate your query method with @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);
补充@chaserb 的答案,我个人会将参数添加为 Java8 可选类型,以使其在方法签名中显式显示作为可选过滤器的语义。
@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);
来不及回答。不确定 Bar 和 Goo 之间的关系。检查 Example 是否可以帮助您。
它对我有用。我有类似的情况,实体 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
}
}
你可以使用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),它将 return 您的实体对象列表(运行 示例中的 Tickets)
ticketRepo.findAll(specification); // Pass output of function in step 2 to findAll
现在回答也太晚了,但对于任何正在寻找解决方案但有更简单方法的人来说,我遇到了同样的问题,最终找到了这个看起来非常简单有效的解决方案其他给我:
我的控制器Class:
@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
作为 @PathVariable
和 length
和 offset
是我需要的参数,但我接受 id
和flags
用于过滤搜索结果,所以它们是我的可选参数,调用 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
条件 不是我的列数据 ,所以如果客户端为我发送 Id
和 flags
参数,我将用它们过滤结果否则我只是用 username
查询这是我的 @PathVariable
.
已经有很多很棒的答案,但我使用@Pankaj Garg 的答案(使用 Spring 规范 API)专门实现了这个。我在回答中添加了一些用例
- 4 个参数可能为空也可能不为空。
- 来自存储库的分页响应。
- 正在按嵌套对象中的字段过滤。
- 按特定字段排序。
首先,我创建了几个实体,特别是 Ticket
、Movie
和 Customer
。这里没什么特别的:
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;
}
然后我创建一个 class,其中包含我希望过滤的参数字段:
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;
}
接下来我创建一个 class 以根据过滤器参数生成一个 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> {
}
最后,在某些服务 class 中,我得到这样的结果:
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);
}
}
PageRequest
和 TicketFilterParam
可能会从休息端点上的一些参数和值中获得。