简介
如果你已经使用了 Spring Data JPA工作过一段时间,你可能已经熟悉了派生查询方法。
@Repository
public interface BookRepository extends MongoRepository<Book, String> {
List<Book> findByAuthor(String name);
}
它们是一种灵巧而快速的方式,通过简单地定义方法名,就可以减轻Spring Data JPA编写查询的负担。
在这个假想的场景中--我们为一个Book 类定义了一个MongoRepository ,该类有一个名为author 的属性,类型为String 。
提醒:
MongoRepository只是一个适合Mongo的专门的PagingAndSortingRepository,而Mongo又是一个专门的CrudRepository。
而不是在实现BookRepository 的服务中实现这个方法--Spring Data JPA会根据方法的名称自动生成一个查询。它将生成一个查询,返回所有Book 记录的列表,并有一个匹配的author 。
一旦该方法被调用并带有一些输入,就会产生以下请求。
find using query:
{ "author" : "Max Tegmark"}
fields: Document{{}}
for class:
class com.example.demo.Book in collection: books
注意:为了查看这个输出,你必须把MongoTemplate 的调试级别设置为DEBUG 。
logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG
这是Spring Data JPA的一个极其灵活和强大的功能,它允许你 bootstrap在不编写查询本身的情况下,甚至在后端实现任何处理逻辑。
然而,当需要复杂的查询时,它们会变得非常难以创建。
public interface PropertyRepository extends MongoRepository<Property, String> {
List<Property> findPropertiesByTransactionTypeAndPropertyType(@Param("transaction_type") TransactionType transactionType, @Param("property_type") PropertyType propertyType);
}
而这仅仅是针对两个参数。当你想创建一个5个参数的查询时会发生什么?
另外,你要创建多少种方法变化?
这时,你很可能想自己写查询。这可以通过@Query 注解来实现。
@Query注释是在MongoRepository接口中的方法级应用的,与一个方法有关。注解中使用的语言取决于你的后端。当然,对于Mongo后端来说,你将会编写本地的Mongo查询,不过,@Query也支持关系型数据库,并接受对它们的本地查询,或者是中性的JPQL(Java Persistence Query Language),它被自动翻译成你使用的数据库的本地查询。
当注解的方法被调用时--来自@Query 注解中的查询会启动并返回结果。
注意:本指南将涵盖Spring Data JPA与Mongo数据库的耦合,并将使用适用于MongoDB的查询。
领域模型和存储库
让我们快速定义一个Book 模型,我们将把它作为资源库的@Document 。为了正确展示各种操作,比如使用Mongo的lt 和gt 操作符,我们将有几个不同的属性。
@Document(collection = "books")
public class Book {
@Id
private String id;
private String name;
private String author;
private long pageNumber;
private long publishedYear;
// Getters, setters, constructor, toString()
}
MongoDB处理String 或ObjectId 类型的ID。这取决于你选择使用哪一个--而且ObjectId 可以很容易地转换为字符串,反之亦然,所以不会有太大的区别。
在任何情况下,让我们为这个模型定义一个简单的BookRepository 。
public interface BookRepository extends MongoRepository<Book, String> {
}
它目前是空的,但考虑到MongoRepository 是CrudRepository 接口的后裔,它在CRUD操作中工作得很好。此外,分页和排序也是开箱即用的。
在接下来的章节中,我们将看看
@Query注解本身,以及MongoDB的查询结构,如何引用方法参数以及排序和分页。
了解@Query注解
@Query 注解是相当简单和直接的。
@Query("mongo query")
public List<Book> findBy(String param1, String param2);
一旦findBy() 方法被调用,就会返回结果。请记住,在编译时,Spring Boot并不预先知道查询将返回什么类型。例如,如果它返回多个结果,而你只有一个预期的返回值--在运行时就会抛出一个异常。
这取决于你是否能确保查询的响应与方法的返回类型相匹配。
你可以在这里有固定或动态的查询。例如,你可以简化之前的方法名称,并将混乱的参数委托给@Query 注解。
@Query("query with param1, param2, param3")
List<Book> findAllActive();
@Query("query with param1, param2, param3")
List<Book> findBy(param1, param2, param3);
在第一个例子中,我们有一套固定的参数,比如说,即使客户没有指定,也总是搜索活动的书籍。这比派生查询方法有优势,因为方法名称很干净。另外,你可以向方法提供参数,然后将其注入到@Query 注释本身。
@Query("{'active':true}")
List<Book> findAll();
@Query("{'author' : ?0, 'category' : ?1}")
List<Book> findPositionalParameters(String author, String category);
@Query("{'author' : :#{#author}, 'category' : :#{#category}}")
List<Book> findNamedParameters(@Param("author") String author, @Param("category") String category);
对于那些可能还没有完全熟悉MongoDB查询结构的人来说,在深入研究提取方法参数并在查询中使用它们之前,让我们先来了解一下这些参数吧
MongoDB查询结构
MongoDB有一个相当直接的查询结构,不过与SQL结构不同。如果你以前没怎么用过MongoDB,如果你已经习惯了关系型数据库,那么最好先温习一下这些结构的记忆。
所有的Mongo查询都发生在大括号之间。
{query}
标准的平等条件遵循一个简单的模式。
{
<field1> : <value1>,
<field2> : <value2>,
...
}
例如,我们可以查询我们的书籍为。
{
author : 'Max Tegmark',
pageNumber : 568,
...
}
这个查询检查集合中所有符合author 和pageNumber 的Book 文档。 你还可以在这里加入运算符。
{
author :
{
$in : ['Max Tegmark', 'Ray Kurzweil']
}
}
这个查询检查author 是否是所提供的任何值。一些支持的运算符有:$gt,$lt,$in,$nin,$or,$and,$nor,$not 和$size ,虽然有很多,但值得去了解它们。例如,这里有一个查询,搜索两个作者中任何一个的所有文件,页数在400和500之间,不是在2018和2019年发布的。
{
author : { $in : ['Max Tegmark', 'Ray Kurzweil']},
pageNumber : { $gt : 400, $lt : 500},
publishedYear : {$nin : [2018, 2019]}
}
这是你在大量查询中需要的大部分查询知识,但在认真使用MongoDB之前,不要跳过对它的实际了解。此外,你很可能还想用聚合的方式工作。
引用命名的和定位的方法参数
有了MongoDB的功能知识,让我们来看看如何引用方法参数。你可以通过它们的名字来引用它们,与@Param 注释和SpEL表达式混合使用,这样更省事,但也更灵活;或者,通过位置参数来引用,由于简单,这通常是首选方法。
@Query("{'author' : ?0, 'category' : ?1}")
List<Book> findPositionalParameters(String author, String category);
@Query("{'author' : :#{#author}, 'category' : :#{#category}}")
List<Book> findNamedParameters(@Param("author") String author, @Param("category") String category);
在第一种方法中,第一个位置参数?0 ,对应于方法中的第一个参数,该参数的值将被使用,而不是?0 。这意味着你必须跟踪这些位置,不要把它们混在一起,否则,MongoDB将默默地失败,只是不会返回结果,鉴于模式的灵活性,因为你还不如拥有这个属性。
提示:如果你已经打开了
DEBUG作为你的日志级别--你将能够在日志中看到发送到Mongo的查询。你可以将该查询复制粘贴到MongoDB Atlas中,以检查该查询是否在那里返回正确的结果,并验证你是否不小心弄乱了位置。有可能的是--你的查询没有问题,但你只是混淆了位置,所以结果是空的。
在第二种方法中,我们使用SpEL表达式来匹配提供的参数和@Query 。你不必以任何特定的顺序来定义它们,因为它们将被按名称而不是按位置来匹配。不过,为了API的可读性,保持一个统一的位置还是有意义的。
让我们在REST控制器中定义一个简单的端点来测试这个方法。
@RestController
public class HomeController {
@Autowired
private BookRepository bookRepository;
@GetMapping("/find")
public ResponseEntity main() {
return ResponseEntity.
ok(bookRepository.findPositionalParameters("Ray Kurzweil", "Fiction"));
}
}
一旦设置好,让我们发送一个curl 请求(或者通过浏览器导航到这个URL)。
$ curl localhost:8080/find
[ {
"id" : "613fba633150f9788cd1858f",
"name" : "Danielle: Chronicles of a Superheroine",
"author" : "Ray Kurzweil",
"pageNumber" : 472,
"publishedYear" : 2019,
"category" : "Fiction"
}
注意:为了获得一个漂亮的打印响应,记得在你的application.properties 中把Jackson的INDENT_OUTPUT 改为true 。
spring.jackson.serialization.INDENT_OUTPUT=true
用Page和Pageable对结果进行分页
由于MongoRepository 扩展了PagingAndSortingRepository ,所以开箱即支持排序和分页!像往常一样,其过程是返回一个Page 类型,并向方法本身提供一个Pageable 。
@Query("{'author' : ?0}")
Page<Book> findBy(String author, Pageable pageable);
当调用该方法时,你需要提供一个有效的Pageable 对象。
@GetMapping("/find")
public ResponseEntity main() {
// PageRequest.of(page, size)
Pageable pageable = PageRequest.of(0, 2);
return ResponseEntity.
ok(bookRepository.findBy("Ray Kurzweil", pageable));
}
在这里,我们要为第一页创建一个PageRequest (基于0的索引),大小为2 文档。如果数据库中有10个符合要求的文档,将返回5个页面,范围从0..4 。
让我们把返回的整个Page 对象打印出来,其中content 包含查询的结果,其他几个与页面有关的属性也存在。在这里你可以看到结果在页面中是如何组织的--即排序、页面大小、页码等。
$ curl localhost:8080/find
{
"content" : [ {
"id" : "613fb60a3150f9788cd18589",
"name" : "The Singularity Is Near",
"author" : "Ray Kurzweil",
"pageNumber" : 652,
"publishedYear" : 2005,
"category" : "Popular Science"
}, {
"id" : "613fba633150f9788cd1858f",
"name" : "Danielle: Chronicles of a Superheroine",
"author" : "Ray Kurzweil",
"pageNumber" : 472,
"publishedYear" : 2019,
"category" : "Fiction"
} ],
"pageable" : {
"sort" : {
"sorted" : false,
"unsorted" : true,
"empty" : true
},
"offset" : 0,
"pageNumber" : 0,
"pageSize" : 2,
"unpaged" : false,
"paged" : true
},
"last" : true,
"totalPages" : 1,
"totalElements" : 2,
"size" : 2,
"number" : 0,
"sort" : {
"sorted" : false,
"unsorted" : true,
"empty" : true
},
"numberOfElements" : 2,
"first" : true,
"empty" : false
}
collect() 如果你想只显示结果,你可以访问数据的Stream<Book> ,并把它变成一个列表。
@GetMapping("/find")
public ResponseEntity main() {
// PageRequest.of(page, size)
Pageable pageable = PageRequest.of(0, 2);
return ResponseEntity.
ok(bookRepository.findBy("Ray Kurzweil", pageable)
.get()
.collect(Collectors.toList()));
}
curl localhost:8080/find
[ {
"id" : "613fb60a3150f9788cd18589",
"name" : "The Singularity Is Near",
"author" : "Ray Kurzweil",
"pageNumber" : 652,
"publishedYear" : "2005-08-31T22:00:00Z",
"category" : "Popular Science"
}, {
"id" : "613fba633150f9788cd1858f",
"name" : "Danielle: Chronicles of a Superheroine",
"author" : "Ray Kurzweil",
"pageNumber" : 472,
"publishedYear" : 2019,
"category" : "Fiction"
} ]
带排序的分页
要用排序来扩展这个功能,你所要做的就是向Sort 对象提供一个PageRequest ,说明你想按哪个属性和哪个顺序来排序。
@GetMapping("/find")
public ResponseEntity main() {
Pageable pageable = PageRequest.of(0, 3,
Sort.by("name").ascending()
.and(Sort.by("pageNumber").ascending()));
return ResponseEntity.
ok(bookRepository.findAll(pageable)
.get()
.collect(Collectors.toList()));
}
在这里,我们按名字升序和页码升序对结果进行排序。当通过多个属性进行排序时,你可以通过and() ,并提供另一个Sort.by() ,将任何数量的属性串联起来 !
findAll() 方法是存在于MongoRepository 接口中的一个默认方法,它同时接受Sort 和Pageable 实例,也可以在没有它们的情况下运行。在这里,我们利用了这一点,使用新的Pageable 来查询。
$ curl localhost:8080/find
[ {
"id" : "613fba633150f9788cd1858f",
"name" : "Danielle: Chronicles of a Superheroine",
"author" : "Ray Kurzweil",
"pageNumber" : 472,
"publishedYear" : 2019,
"category" : "Fiction"
}, {
"id" : "613fb6933150f9788cd1858e",
"name" : "Our Mathematical Universe",
"author" : "Max Tegmark",
"pageNumber" : 432,
"publishedYear" : 2014,
"category" : "Popular Science"
}, {
"id" : "613fb60a3150f9788cd18589",
"name" : "The Singularity Is Near",
"author" : "Ray Kurzweil",
"pageNumber" : 652,
"publishedYear" : 2005,
"category" : "Popular Science"
} ]
在这里,第一个属性占优先地位!尽管第二本书的页数比第一本书少,而且我们已经按升序页数排序,但按名字排序的结果是这样的。如果按名字排序是不明确的,那么第二个属性就会被选中。
带操作符的查询
说了这么多,让我们重新创建文章开头的查询。
{
author : { $in : ['Max Tegmark', 'Ray Kurzweil']},
pageNumber : { $gt : 400, $lt : 500},
publishedYear : {$nin : [2018, 2019]}
}
这就像把这个查询复制到@Query 注释中一样简单!我们知道我们有三本书,其中一本有652页,而且其中一本是在2019年出版的--我们应该期望这里只返回一本--马克斯-泰格马克的*《我们的数学宇宙》*!让我们测试一下这是否符合我们的期望。
让我们测试一下这是否是真的。
@Query("{\n" +
"author : { $in : ?0},\n" +
"pageNumber : { $gt : ?1, $lt : ?2},\n" +
"publishedYear : {$nin : ?3}\n" +
"}")
List<Book> findBy(String[] authors, int pageNumLower, int pageNumUpper, int[] excludeYears);
或者,对于一个更简洁的实现。
@Query("{'author' : { $in : ?0}, 'pageNumber' : { $gt : ?1, $lt : ?2},'publishedYear' : {$nin : ?3}}")
List<Book> findBy(String[] authors, int pageNumLower, int pageNumUpper, int[] excludeYears);
**注意:**当提供数据的数组时,例如authors 和excludeYears - 在查询中没有必要将参数定义为数组 -[?0] 。这将在一个数组中创建一个数组。@Query 注解会自动将你的输入转换为正确的查询。
让我们更新端点并提供一些数据。
@GetMapping("/find")
public ResponseEntity main() {
return ResponseEntity.
ok(bookRepository.findBy(
new String[]{"Ray Kurzweil", "Max Tegmark"}, // Authors
400, // Lower pageNumber bound
500, // Upper pageNumber bound
new int[]{2018, 2019})); // Exclusion years
}
而当我们向它发送请求时。
$ curl localhost:8080/find
[{
"id" : "613fb6933150f9788cd1858e",
"name" : "Our Mathematical Universe",
"author" : "Max Tegmark",
"pageNumber" : 432,
"publishedYear" : 2014,
"category" : "Popular Science"
}]
就像时钟一样。
总结
在本指南中,我们已经在Spring Data MongoDB的背景下看了一下@Query 注解。
该注解允许你为各种数据库(关系型和非关系型)定义自己的查询,包括本地和JPGL。我们选择了使用本地Mongo查询来与非关系型数据库进行交互。在为它定义了一个模型和一个存储库之后,我们已经探索了MongoDB使用的查询结构,以及@Query 注释的一般工作方式。随后,我们参考了命名的和定位的方法参数,对查询结果进行分页和排序,以及如何使用MongoDB操作符来构造更复杂的查询!