这就是我滚动的方式2014年3月23日在春天mvc | 规格 | 春季资料 | jpa | 参数解析器 | java | 搜索 | 搜索 | 滤波
Spring MVC&Spring Data JPA过滤数据的另一种API
更新 自从我写这篇文章以来已经有一段时间了。我仍然认为它值得阅读,但请务必检查Github页面,因为所描述的库已经发展,并已成为Maven Central中的一个完整的开源项目。
我坚信,一个卓越的框架最终会成为一种(领域特定的)语言。
我已经使用了Spring MVC好几年了,但是我仍然对你定义控制器的灵活性印象深刻。它不再只是一堆实现的接口,而是一组构建块,使程序员能够流利地表达他们的Web应用程序。这更好,因为Spring给了我们一个API来扩展它。
Spring Data Web是使用此API来扩展Spring MVC控制器定义“语言”的一个很好的例子。例如,它提供了一个HandlerMethodArgumentResolver
允许你使用一个Pageable
对象作为你的控制器方法的参数。这很酷 - 不需要手动绑定HTTP参数来处理分页,这在几乎所有的企业系统中都是存在的。
实际上,在处理表格数据时,我们通常必须实现另一个功能(排序和分页除外)。这是数据过滤(或搜索)。通过自定义HandlerMethodArgumentResolver
,可以以通用和声明的方式进行处理。我接受了实施的挑战,并希望与您分享结果。
通过单个属性进行过滤
最简单的例子是通过一个属性来查找实体。假设我们有Customer
实体,我们希望找到提供名字的所有客户。网络请求可能如下所示:
GET http://myhost/api/customers?firstName=Homer
在我实现的微库中,相应的控制器方法可以具有如下形式:
@RequestMapping(value = "/customers", params = "firstName")
public Iterable<Customer> findByFirstName(
@Spec(path = "firstName", spec = Like.class) Specification<Customer> spec) {
return customerRepo.findAll(spec);
}
正如你所看到的,它是普通的Spring MVC和Spring Data加上一个自定义@Spec
注释。注释指向Like
实现谓词的类(在这种情况下)。谓词通过应用程序配置中指定的自定义参数解析器自动实例化。春数据储存库(customerRepo
)使用所提供的标准来执行与以下where子句的查询:where firstName like ‘%Homer%‘
。
我们来看看这个例子中使用的组件:
- 参数是类型的
Specification<Customer>
。这是JPA标准API的更高层包装。Spring Data JPA提供的接口是由 Eric Evan的领域驱动设计书中介绍的规范模式导出的。 - 该
@Spec
注释包含定义Web请求的解释的元数据。该路径属性指定过滤实体的财产。默认情况下,过滤模式预期为具有与属性路径(本例中为firstName)相同名称的Web参数的值。注释的spec属性指向要使用的
Specification
实现。在这种情况下Like
,对于指定路径,它将与使用like %pattern%
where子句提供的模式相匹配。 - 该方法
Specification
由Spring MVC和自定义自动提供HandlerMethodArgumentResolver
。解析器本身处理注释并访问Web请求参数以实例化适当的Specification
对象。解析器的实际实现可以在Github上的回购中找到。我没有在这里介绍代码,因为它实际上只是注释处理和一些反射。
规格
让我们仔细看看这个Specification
s。Like
课程实施如下:
public class Like<T> extends PathSpecification<T> {
private String pattern;
public Like(String path, String... args) {
super(path);
if (args == null || args.length != 1) {
throw new IllegalArgumentException(
"Expected exactly one argument (a fragment to match against)");
} else {
// (caution! some additional validation is required!)
this.pattern = "%" + args[0] + "%";
}
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> q, CriteriaBuilder cb) {
return cb.like(this.<String>path(root), pattern);
}
}
重要的部分是:
- 你可能想知道为什么构造函数没有明确地使用一个模式参数,而是一个名为args的字符串数组。这是为了保持参数解析器独立
Specification
于未来添加的任何新的实现(Open / Closed原理)。解析器只是从注解的spec参数中获取一个类,并使用反射调用其构造函数。它没有意识到它实例化的类的任何其他细节,这允许你Specification
自由地添加一个新的。 toPredicate
方法使用从Web请求中获取的路径和模式(基于注释元数据)。这是常规的JPA标准代码。该path
方法在超类中实现,其目标是解析像"firstName"
或"address.street"
到JPA表达式的字符串。- 为了关注一般假设,我跳过了一些额外的验证。在许多情况下,你想检查模式(并强制至少3个字符),以避免不受限制的查询。
同样,我们可以实现任何其他的规范来处理不同谓词的查询。
当web参数不存在时
思考如果请求中不存在firstName参数,会发生什么情况是值得的。那么,@RequestMapping
这个参数是明确指定的,所以它是映射的一部分 - 如果它不存在,控制器方法将不会被调用。但是另一种方法就是像这样对方法进行注释:
@RequestMapping("/customers") // no params required
...
那么我们可以假设这些请求:
GET http://myhost/api/customers
GET http://myhost/api/customers?firstName=Homer
应该返回所有分别以名字过滤的客户和客户。
这是一个优雅的解决方案。参数解析器在指定的web参数不存在的情况下返回null@Spec
。空指定由Spring Data作为空标准处理,因此根本不需要过滤。所以下面的方法处理这两种情况:
@RequestMapping("/customers") // no ‘params‘ argument
public Iterable<Customer> findByFirstName(
@Spec(path = "firstName", spec = Like.class) Specification<Customer> spec) {
return customerRepo.findAll(spec);
}
当参数名称不同于属性名称时
默认情况下,Web参数名称与属性的属性路径相同。这很方便,但有时候我们想要重写这个规则来使参数名称更加明确。例如,假设我们想要查找在某个日期之前注册的所有客户。网络请求可能如下所示:
GET http://myhost/api/customers?registeredBefore=2014-03-10
在这种情况下,说registerBefore而不是仅使用属性名称(registrationDate)更有意义。处理上面的映射看起来像:
@RequestMapping("/customers")
public Iterable<Customer> findByRegistrationDate(
@Spec(params = "registeredBefore"
path = "registrationDate", spec = DateBefore.class) Specification spec) {
return customerRepo.findAll(spec);
}
正如你所看到的,它使用了一个额外的属性(params)来指定web参数的名字。
另一个有趣的事情是在片段中使用的规范 - DateBefore
。这个实现与Like
之前描述的非常类似,但是您可能会对日期时间格式感到疑惑。这是一个好点!DateBefore
类使用一些默认的格式,可以使用注释的可选config属性来重写@Spec
:
@Spec(path="registrationDate", params="registeredBefore",
spec=DateBefore.class, config="yyyy-MM-dd")
...
解析器可以将这样的附加信息传递给规范构造器。
使用多个参数来构建规范
到目前为止,我们已经看到每个规范只使用一个web参数的例子。它不一定是这样的。需要两个参数的规范的一个很好的例子是表达式之间的日期,即where date between :after and :before
。它可以很容易地处理,因为params属性@Spec
可以接受一串字符串。控制器定义将变成:
@RequestMapping("/customers")
public Iterable<Customer> findByRegistrationDate(
@Spec(params = {"registeredBefore", "registeredAfter"},
path = "registrationDate", spec = DateBetween.class) Specification spec) {
return customerRepo.findAll(spec);
}
通过多个属性进行过滤
有时我们想同时过滤多个属性。例如这个请求:
GET http://myhost/api/customers?firstName=Homer&lastName=Simpson
应通过firstName和lastName缩小客户名单。为了处理这样的请求,控制器方法可以被声明如下:
@RequestMapping(value = "/customers/", params = { "firstName", "lastName" })
public Iterable<Customer> find(
@And(
@Spec(path = "firstName", spec = Like.class),
@Spec(path = "lastName", spec = Like.class)) Specification spec) {
return customerRepo.find(spec);
}
正如你所看到的,这个参数现在被注释了@And
,这是一个围绕多个@Spec
注释的包装。结果,解决的规范转化为where firstName like ‘%Homer%‘ and lastName like ‘%Simpson%‘
。我们可以使用@Or
类似的注解来定义一个析取。
匹配单个Web参数的多个路径
通常需要一个搜索引擎,将所提供的短语与实体的多个属性进行匹配。例如,当用户输入“8976”时,首先检查客户号码,然后是增值税号码。结果集应该包括所有的客户,其中任何一个属性都包含给定的模式。搜索引擎足够用户友好,用户不必明确选择搜索条件。相应的请求可能只有一种形式:
GET http://myhost/api/customers?query=8976
以上示例中描述的API足以定义处理查询的方法:
@RequestMapping(value = "/customers")
public Iterable<Customer> find(
@Or({
@Spec(params="query", path="customerNumber", spec=Like.class),
@Spec(params="query", path="vatId", spec=Like.class)}) Specification spec){
return customerRepo.find(spec);
}
而已。我希望在这一点上面的声明很容易理解。
更复杂的查询
我实现的解析器处理更复杂的定义 - 例如,您可以@Or
在一个内部有多个注释@And
。我没有在这里展示这样的例子,但是如果你愿意,你可以在Github上的回购中找到它们。
或者,您可以Specification
在单个控制器方法中使用多个参数(使用不同的注释),然后以编程方式合并它们。你也可以在Github上找到一个样本。
结论
在这篇文章中,我介绍了一个用于声明式解析Specfication
Spring MVC控制器处理程序方法参数的API 。提出的实现是HandlerMethodArgumentResolver
使用注释元数据和Web请求参数的自定义。我成功地在我的一个项目中使用它。我认为在寻找方法不是很复杂但很多的情况下,这可能是一个节省时间的方法。
此外,我认为这是一个有益的练习设计和实施这样的API。Spring为我们提供了很多机制来扩展它。HandlerMethodArgumentResolver
接口就是其中之一,我鼓励您将其视为一种减少控制器中重复代码量的方法。
来源
- 这个库的源代码可以在Github上找到:https://github.com/tkaczmarzyk/specification-arg-resolver。阅读README.md和CHANGELOG.md文件是值得的,因为它们可能包含了本文写作后引入的新功能的描述。
- 您可以从Maven Central存储库下载二进制发行版:
<dependency> <groupId>net.kaczmarzyk</groupId> <artifactId>specification-arg-resolver</artifactId> <version>0.6.0</version> </dependency>
- 或者,您可以从我的私有Maven仓库获取最新的快照:
<repository> <id>kaczmarzyk.net</id> <url>http://repo.kaczmarzyk.net</url> </repository>
依赖性定义是:
<dependency> <groupId>net.kaczmarzyk</groupId> <artifactId>specification-arg-resolver</artifactId> <version>0.6.0</version> </dependency>
- 一个简单的Web应用程序使用库可以在这个回购:https://github.com/tkaczmarzyk/specification-arg-resolver-example
它建立在Spring Boot的基础之上,所以你不需要任何服务器来运行它 - 只需将它作为独立的Java应用程序来运行,并开始监听
http://localhost:8080
。它公开了用于客户数据库过滤的REST API。
Tomasz Kaczmarzyk
软件开发人员。Java,Scala和开源爱好者。相信测试代码至少和生产代码一样重要。热切地研究新技术。
分享这篇文章
所有内容版权,这是我如何滚动 ?2014-2015?保留所有权利。自豪地与鬼发表