SpringDataMongoDB-2
定义Repository接口
这是使用SpringData MongoDB的第一步,先定义 Repository。这个接口需要传递两个类型参数,域对象和主键的类型。就像上一节例子的那样。
增加顶级接口
一般来说都是直接继承Repository 或者CrudRepository ,或者具体的SpringData各个模块自己实现的Repository 的子类。但是如果自己想要做一个顶级的接口,项目中的各个Repository继承与它来做处理。需要按照下面的步骤来操作。
-
自定义接口,增加方法,继承于Repository . -
再接口上面标注@NoRepositoryBean ,表示不会为这个接口创建对应的bean。
不继承Repository接口
如果不想继承Repository接口和它的子接口。可以自己写接口,标注@RepositoryDefinition(domainClass = Book.class, idClass = String.class) 注解,想要用CrudRepository 接口中的哪些方法,直接拷贝过来就行。
@RepositoryDefinition(domainClass = Book.class, idClass = String.class)
public interface BookCustomerRepository {
Optional<Book> findById(String id);
Iterable<Book> saveAll(Iterable<Book> entities);
}
测试类如下:
@SpringBootTest(classes = SpringdataMongodbApplication.class)
public class EtcTest {
@Autowired
private BookCustomerRepository bookCustomerRepository;
@Test
public void testCustomerInterface(){
ArrayList<Book> param = new ArrayList<>();
List<String> bookName = Arrays.asList("java", "go", "php", "c", "c++", "Mysql");
ThreadLocalRandom current = ThreadLocalRandom.current();
LocalDate today = LocalDate.now();
for (int i = 0; i < 1000; i++) {
String name = bookName.get(current.nextInt(bookName.size()));
Book book = new Book().setCount(current.nextInt(1000))
.setName(name)
.setPrice(current.nextDouble(100))
.setPublishTime(today.minusDays(current.nextInt(30)))
.setCount(current.nextInt(1000))
.setShortName(name + ":" + name);
param.add(book);
}
Iterable<Book> books = bookCustomerRepository.saveAll(param);
Book book = books.iterator().next();
Optional<Book> bookOptional = bookCustomerRepository.findById(book.getId());
Assert.isTrue(bookOptional.isPresent());
}
}
SpringData多个模块一块使用
SpringData有多个模块,可能会一块使用,比如一个项目里面有Spring Data JPA,Spring Data MongoDB,Spring Data Redis。要是只用一个模块还好,所有的repository只会关联到这一个。但是多个一块工作的时候,就需要将他们区分,当探测到classpath上有多个Spring Data模块,SpringData就会进入严格的配置模式。
需要通过下面三种方式来区分
- 继承每个模块专有的repository,比如继承
JpaRepository 或者MongoRepository ,这样的区分就很明显,前者是JPA,后者是MongoDB。 - 使用每个模块专有的注解,比如
@Entity 和@Document 。前者是JPA,后者是MongoDB。 - 再配置类上使用
@Enable${store}Repositories 注解,比如EnableJpaRepositories 和EnableMongoRepositories ,可以利用他们来指定具体的扫描的包,比如@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa") 表示这个包下面使用的是jpa。
定义查询方法
查询方法是日常使用中变种最多的,用途最广的。别的方法在CrudRepository 中定义了,基本也够用。
可以在自定义的Repository中写方法,这些方法不需要自己实现,只要满足SpringData的语法规则,它会自己帮我们实现
- 实体类
@Document
@Data
@Accessors(chain = true)
@ToString
public class Book {
private String id;
@Field(name = "name_1")
private String name;
private String shortName;
private Integer count;
private Double price;
private LocalDate publishTime;
}
- Repository
public interface BookQueryMethodRepository extends MongoRepository<Book, String> {
Book findBookByName(String name);
List<Book> findBooksByNameAndCount(String name,int count);
Optional<List<Book>> findBooksByNameLikeAndCountGreaterThan(String name, int count);
List<Book> findDistinctByCountInAndNameIs(List<Integer> count,String name);
List<Book> findByNameIgnoreCase(String name);
}
- 测试类
@SpringBootTest(classes = SpringdataMongodbApplication.class)
public class BookQueryMethodRepositoryTest extends BaseApplicationTests {
@Autowired
BookQueryMethodRepository bookQueryMethodRepository;
@Test
public void testSave() {
ArrayList<Book> param = new ArrayList<>();
List<String> bookName = Arrays.asList("java", "go", "php", "c", "c++", "Mysql");
ThreadLocalRandom current = ThreadLocalRandom.current();
LocalDate today = LocalDate.now();
for (int i = 0; i < 1000; i++) {
String name = bookName.get(current.nextInt(bookName.size()));
Book book = new Book().setCount(current.nextInt(1000))
.setName(name)
.setPrice(current.nextDouble(100))
.setPublishTime(today.minusDays(current.nextInt(30)))
.setCount(current.nextInt(1000))
.setShortName(name + ":" + name);
param.add(book);
}
bookQueryMethodRepository.saveAll(param);
}
@Test
public void testDelete() {
bookQueryMethodRepository.deleteAll();
}
@Test
public void testFind1() {
Book book = bookQueryMethodRepository.findBookByName("c++");
System.out.println(book);
}
@Test
public void testFind2() {
List<Book> booksByName = bookQueryMethodRepository.findBooksByNameAndCount("c++", 10);
booksByName.forEach(System.out::println);
}
@Test
public void testFind3() {
Optional<List<Book>> booksOption = bookQueryMethodRepository.findBooksByNameLikeAndCountGreaterThan("ja", 10);
booksOption.ifPresent(System.out::println);
}
@Test
public void testFind4() {
List<Book> goBooks = bookQueryMethodRepository.findDistinctByCountInAndNameIs(Arrays.asList(360, 802), "go");
goBooks.forEach(System.out::println);
}
@Test
public void testFind5() {
List<Book> goBooks = bookQueryMethodRepository.findByNameIgnoreCase("GO");
goBooks.forEach(System.out::println);
}
}
-
配置文件 spring:
application:
name: mongoDB-test
data:
mongodb:
username: root
password: root
authentication-database: admin
host: localhost
port: 27017
database: test
server:
port: 8080
建议可以对比MongoDB的查询语句的结果来验证是否正确
方法名称规则说明
方法名称可以分为对象和判断条件,第一部分(find…by,exists…By)定义了这次查询的对象,在find和by或者其他的类似关键字中,除了关键字之外,别的都是描述性的。
从By之后,后面的一部分为判断条件,在这些判断条件之间可以用And或者Or联系起来。
具体的可以看:
Repository query keywords
Supported query method predicate keywords and modifiers
上面不确定的时候查一查,有一些基本的规则知道的话,写代码是没有问题的,此外Idea还有提示。
-
方法名称中的判断条件是属性串联关系组成的,比如可以将属性的表达式用And或者Or来连接在一起,方法的参数和什么条件查询的顺序对应。对于不同类型的属性还可以用不同的操作符,比如Between,LessThan,GreaterThan ,这只是语法定义规则,具体的实现还得看不同模块。 -
查询方法常用几个关键词开头find…By,query…By,,get…By。 -
还可以在属性增加OrderBy来指定排序,后面接Asc 或者Desc 表示升降序。可以在find或者get、query之后用Top 或者First 来限制返回的数量,比如Top10,返回的是前十条,如果只是一个First,默认返回一条。例子如下:
List<Book> findTop10ByNameOrderByCountAsc(String name);
测试类 public class BookQueryMethodRepositoryTest extends BaseApplicationTests {
@Autowired
BookQueryMethodRepository bookQueryMethodRepository;
@Autowired
MongoTemplate mongoTemplate;
@Test
public void testFind6() {
Criteria criteria = Criteria.where("name_1").is("go");
Query query = Query.query(criteria)
.with(Sort.sort(Book.class).by(Book::getCount).ascending())
.limit(10);
List<Book> books = mongoTemplate.find(query, Book.class);
List<Book> go = bookQueryMethodRepository.findTop10ByNameOrderByCountAsc("go");
int index = 0;
while (index < books.size()) {
Assert.isTrue(books.get(index).getId().equals(go.get(index).getId()));
index++;
}
Assert.isTrue(index == books.size() && index == go.size());
}
}
对应的MongDO查询语句 db.book.find(
{
name_1:"go"
}
).sort(
{
count:1
}
).limit(10)
-
还可以增加Pageable ,Sort 参数来做查询和分页。返回值可以是Page ,Slice ,List public interface BookQueryMethodRepository extends MongoRepository<Book, String> {
Slice<Book> findBooksByName(String name, Pageable pageable);
}
测试类 @Test
public void testFind7() {
Sort sort = Sort.sort(Book.class)
.by(Book::getCount)
.ascending();
PageRequest request = PageRequest.of(1, 10, sort);
Slice<Book> books = bookQueryMethodRepository.findBooksByName("go", request);
books.get().forEach(System.out::println);
}
对应的MongDO查询语句 db.book.find(
{name_1:"go"}
)
.sort({count:1})
.skip(10).limit(10);
? Page和Slice的区别:
page对象知道元素的总数和页数,他是通过一个基础的查询计数来计算的,所以它比较费时间.
Slice只知道下一个Slice是否可用,在返回大量数据集合的时候就比较方便了.
查询方法的返回值
大体的分为下面几种:(具体的可以看Supported Query Return Types)
- 返回集合或者可以迭代的对象
? 查询的方法支持Java原生的Iterable ,List ,Set ,同时也支持Spring的Streamable ,Iterable 的实现类,还可以返回 Vavr
Collection<Book> findByNameEndingWith(String name);
Streamable<Book> findByShortNameEndsWith(String name);
BookStream findByPriceLessThanEqual(double price);
注意说明
-
Streamable是Spring提供的一个函数式接口,通过它可以很方便的聚合,过滤. -
BookStream实现了Streamable接口,增加了一些自定义的方法,想要这样用的话,需要暴露一个构造函数或者静态工厂方法将Streamable作为参数传递进去,方法的名字是of(…)或者valueOf(…) .下面是我自己实现的代码举例 @RequiredArgsConstructor(staticName = "of")
public class BookStream implements Streamable<Book> {
private final Streamable<Book> streamable;
@Override
public Iterator<Book> iterator() {
return streamable.iterator();
}
public int getTotal() {
return streamable.stream()
.map(Book::getCount)
.reduce(0, Integer::sum);
}
}
测试类
@Test
public void testFind9(){
Streamable<Book> bookStreamable = bookQueryMethodRepository.findByShortNameEndsWith("o");
Collection<Book> bookStreamable1 = bookQueryMethodRepository.findByNameEndingWith("va");
Streamable<Book> streamable = bookStreamable.and(bookStreamable1);
Map<String, List<Book>> collect = streamable.stream()
.collect(Collectors.groupingBy(Book::getShortName));
collect.forEach((key, value) -> {
System.out.println(key);
System.out.println(value.size());
});
}
@Test
public void testFind10(){
BookStream bookStream = bookQueryMethodRepository.findByPriceLessThanEqual(10);
System.out.println(bookStream.getTotal());
}
testFind10对应的MongoDB的语法
db.book.aggregate(
[
{
$match:{
price:{$lte:10}
}
},
{
$group:{
_id:null,
count:{$sum:"$count"}
}
}
]
)
-
返回Optional或者Option 所有CRUD的方法都支持返回java8中的Optional,同样也支持如下的几个类型
- com.google.common.base.Optional
- io.vavr.control.Option
- scala.Option
注意 查询方法可以选择不适用任何的包装的类型,没有结果就直接返回null,但是对于返回collections,或者collection的包装类,streams没有结果不会返回null. Book findByNameIsAndCountGreaterThanAndPriceIs(String name,int count,double price);
测试类 @Test
public void testFind11(){
Book book = bookQueryMethodRepository.findByNameIsAndCountGreaterThanAndPriceIs("小红", 12, 12);
Assertions.assertNull(book);
}
-
返回异步对象 返回类型可以是Future 和CompletableFuture 和ListenableFuture ,需要用@Async注解.实际上会将这个查询操作提交个Spring TaskExecutor.然后立即返回.
@Async
ListenableFuture<List<Book>> findByNameLike(String name);
测试类 @Test
public void testFind12(){
ListenableFuture<List<Book>> goFuture = bookQueryMethodRepository.findByNameLike("go");
goFuture.addCallback(new ListenableFutureCallback<List<Book>>() {
@Override
public void onFailure(Throwable ex) {
ex.printStackTrace();
}
@Override
public void onSuccess(List<Book> result) {
Assert.notEmpty(result);
}
});
}
-
返回单个对象 在上面的例子中已经说了,这里就不再说了. -
返回Page,Slice对象. 上面已经说了,这里就不再说了. -
返回Stream对象. 可以返回Java8的Stream对象.按照递增的方式来处理. Stream<Book> findByCountGreaterThanEqualAndNameIs(int count,String name);
测试类 @Test
public void testFind13(){
try (Stream<Book> goStream = bookQueryMethodRepository.findByCountGreaterThanEqualAndNameIs(20, "go")){
System.out.println(goStream.count());
}
}
Stream要记得关
删除方法
相比查询,删除和更新就比较简单了,除了CrudRepository 提供的一些方法之外,它也是可以像查询方法一样,自定义方法签名,SpringData-MongoDB帮我们实现.对于删除操作是以remove或者delete开头的
Book deleteByIdIs(String id);
int removeById(String id);
List<Book> removeBookByNameIs(String name);
测试类
@Test
public void testDelete1(){
Book book = bookQueryMethodRepository.findByNameIgnoreCase("go").get(0);
Book book1 = bookQueryMethodRepository.deleteByIdIs(book.getId());
Assertions.assertEquals(book1,book);
}
@Test
public void testDelete2(){
List<Book> book = bookQueryMethodRepository.findByNameIgnoreCase("java");
List<Book> books= bookQueryMethodRepository.removeBookByNameIs("java");
Assertions.assertArrayEquals(book.toArray(new Book[]{}),books.toArray(new Book[]{}));
}
需要注意,如果查询的结果不是唯一的,但返回值确实唯一的,比如返回值是Book,那这个方法会报错
更新
更新操作在CrudRepository 接口中并没有定义,但是它的Save方法却有替换的功能,如果_id字段一样,就会替换掉.在后面的文章中会介绍MongoTemplate 的使用,它里面提供了很多的方法.
@Test
public void testUpdate1() {
List<Book> byNameIgnoreCase = bookQueryMethodRepository.findByNameIgnoreCase("c++");
Book book = byNameIgnoreCase.get(0)
.setName("c++ =====+1");
bookQueryMethodRepository.save(book);
List<Book> res = bookQueryMethodRepository.findByNameIgnoreCase("c++ =====+1");
Assert.isTrue(Objects.equals(res.get(0).getId(), book.getId()));
}
SpringData MongoDB中接口中定义方法介绍的差不多了,这些方法都不需要我们手动来实现,SpringData MongoDB会自己帮我们实现.除了这些方法之外,他还提供了MongoTemplate ,他更加的灵活.后续的文章会介绍如果使用MongoTemplate,如果自定义Repository ,如果通过Example来查询,如果做聚合操作,创建索引等等…
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。
|