在学习了Spring Data MongoDB的背景下了解@Query

1,425 阅读9分钟

简介

如果你已经使用了 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的ltgt 操作符,我们将有几个不同的属性。

@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处理StringObjectId 类型的ID。这取决于你选择使用哪一个--而且ObjectId 可以很容易地转换为字符串,反之亦然,所以不会有太大的区别。

在任何情况下,让我们为这个模型定义一个简单的BookRepository

public interface BookRepository extends MongoRepository<Book, String> {
}

它目前是空的,但考虑到MongoRepositoryCrudRepository 接口的后裔,它在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, 
... 
}

这个查询检查集合中所有符合authorpageNumberBook 文档。 你还可以在这里加入运算符。

{
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 接口中的一个默认方法,它同时接受SortPageable 实例,也可以在没有它们的情况下运行。在这里,我们利用了这一点,使用新的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);

**注意:**当提供数据的数组时,例如authorsexcludeYears - 在查询中没有必要将参数定义为数组 -[?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操作符来构造更复杂的查询!