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 {
// 这俩都是从CrudRepository中拷贝过来的,只是将泛型去掉,变为实际的类型
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") // 这里我用@Field做映射了,
private String name;
private String shortName;
private Integer count;
private Double price;
private LocalDate publishTime;
}
- Repository
public interface BookQueryMethodRepository extends MongoRepository<Book, String> {
// 通过名字来查询student,
Book findBookByName(String name);
// 通过name和Count查询,要注意,参数位置是对应的
List<Book> findBooksByNameAndCount(String name,int count);
// name like 并且 count >
Optional<List<Book>> findBooksByNameLikeAndCountGreaterThan(String name, int count);
// count in and name=
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联系起来。
具体的可以看:
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,默认返回一条。例子如下:// 查找name 通过count升序排序,取前10个 List<Book> findTop10ByNameOrderByCountAsc(String name);
测试类
public class BookQueryMethodRepositoryTest extends BaseApplicationTests { @Autowired BookQueryMethodRepository bookQueryMethodRepository; @Autowired MongoTemplate mongoTemplate; // 提供的很方便的操作MOngoDB的工具类。 @Test public void testFind6() { // 通过mongoTemplate来做查询,用来做对比 Criteria criteria = Criteria.where("name_1").is("go"); Query query = Query.query(criteria) // 通过count升序排序 .with(Sort.sort(Book.class).by(Book::getCount).ascending()) .limit(10); List<Book> books = mongoTemplate.find(query, Book.class); // 名字是go,count升序排序,limit 10个 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() { //PageRequest聚合了sort 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
Collection<Book> findByNameEndingWith(String name);
// 返回Spring的Streamable
Streamable<Book> findByShortNameEndsWith(String name);
// 返回自己包装的Streamable的实现类
BookStream findByPriceLessThanEqual(double price);
注意说明
Streamable是Spring提供的一个函数式接口,通过它可以很方便的聚合,过滤.
BookStream实现了Streamable接口,增加了一些自定义的方法,想要这样用的话,需要暴露一个构造函数或者静态工厂方法将Streamable作为参数传递进去,方法的名字是
of(…)或者valueOf(…)
.下面是我自己实现的代码举例@RequiredArgsConstructor(staticName = "of") //lombok注解,提供静态方法,名字是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); //聚合
//通过shortname分组,看看有多少个
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(){
// 自己增加了getTotal的方法
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); // 这肯定是没有的.返回的是null Assertions.assertNull(book); }
-
返回异步对象
返回类型可以是
Future
和CompletableFuture
和ListenableFuture
,需要用@Async注解.实际上会将这个查询操作提交个Spring TaskExecutor.然后立即返回.// 增加ListenableFuture @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);
// 再次查找,看id是否一致
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来查询,如果做聚合操作,创建索引等等…
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。