前言

Spring Data

Spring MongoDB 与Spring Framewrok 提供的 JDBC 十分相似,在熟悉本篇文章之前,需要先熟悉 MongoDB 和Spring 的概念

Spring Data 使用了 Spring 框架的核心功能,包括:

  • IOC容器 (IOC container)
  • 类型转换系统 (type conversion system)
  • EL表达式 (expression language)
  • JMX集成
  • Dao异常层次结构

MongoDB

MongoDB作为一种 NOSQL 工具,非 RDMBS 设计范式,官方文档:https://docs.mongodb.com/manual/reference/operator/query/in/index.html

RDMBS设计范式:http://blog.51cto.com/echoroot/1953996

Spring Data MongoDB 2.0

  • 升级至 java8
  • 使用 Document API,而非 DBObject
  • 支持聚合结果流 Stream
  • Kotlin 扩展
  • 支持隔离 Update 操作
  • 使用 Spring 的 @NonNullApi 和 @Nullable 保证 Null 安全

Spring Data MongoDB 支持的注解

1
2
3
4
5
6
7
8
@Document   : 文档标识,将 java 类与 Collection 文档对应
@Id : 文档的唯一标识,在 mongodb 中为 ObjectID,生成规则:时间戳+机器标识+进程ID+自增计数器(确保同一时间内ID不会冲突)
@Field : 属性注解
@Indexed : 索引
@CompoundIndex : 混合索引
@GeoSpatialIndexed : 声明该字段为地理信息的索引
@Transient : 映射忽略的字段 (即不会保存到 mongodb)
@Query :查询

依赖

在使用 SpringDataMongoDB 之前,需要先声明对 SpringData 模块的依赖关系。

既然 SpringData 存储库抽象中的中央接口是 Repository 。 该接口的子类 CrudRepository 实现了实体类的 CRUD 功能,如果需要的话,也可以通过继承该接口来拓展 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {

<S extends T> S save(S entity);

Optional<T> findById(ID primaryKey);

Iterable<T> findAll();

long count();

void delete(T entity);

boolean existsById(ID primaryKey);

// … more functionality omitted.
}

除了 CrudRepository 之外,还有 JpaRepository 和 MongoRepository。在 CrudRepository 中,有许多抽象方法添加了额外的方法来简化对实体的分页访问。

1
2
3
4
5
6
7
8
9
10
/**
* PagingAndSortingRepository 方法提供了分页和排序的功能
*/
public interface PagingAndSortingRepository<T, ID extends Serializable>
extends CrudRepository<T, ID> {

Iterable<T> findAll(Sort sort);

Page<T> findAll(Pageable pageable);
}

还有包含:删除查询、计数、查询等相关接口

定义自己的 Repository

声明扩展 Repository 或者其子接口之一的接口,并嵌入需要处理的对象和 ID 类型:

1
2
3
interface UserRepository extends MongoRepository<User,String>{  // UserRepository 默认继承了父类的 CRUD 方法
List<User> findByName(String name); // 扩展的查询方法
}

repository 方法的 Null 处理

从 Spring Data 2.0 开始,返回单个聚合实例的 Repository 的 CRUD 方法 可以使用 Java8 中的Optional 来只是可能缺少的值,支持返回一下的包装类型:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option
  • javaslang.control.Option (不推荐)

或者,不使用包装类型,直接返回查询结果为 Null。 使用 Optional 的好处在于保证了方法返回的对象永远不会为 Null,而是相应的 空表示。

多 Spring Data 模块的 Repository

1
2
3
4
5
6
7
8
9
10
interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {

}

interface UserRepository extends MyBaseRepository<User, Long> { // MyRepository 与 MyBaseRepository 都继承了 JpaRepository ,所以它们是有效的子类

}
1
2
3
4
5
6
7
8
9
10
11
12
13
interface AmbiguousRepository extends Repository<User, Long> {

}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {

}

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> {
// 而且多个 SpringData 模块导致无法区分这些 Repository 应该绑定到哪个特定的 仓库

}

定义查询方法

仓库代理有两种方式导出指定的查询。

  • 从名字直接导出查询 : 类似 findByName(String name);
  • 手工定义的查询 : 类似 @query(“{ name : ?0}”)

查询定义策略:
通过 xml 文件中的 query-lookup-strategy 参数或者 Enable 注解中的 queryLookupStrategy 参数。

  • CREATE 尝试从方法名中构造指定仓库的查询方法
  • USE_DECLARED_QUERY 尝试找到声明的查询,若无则抛出异常
  • CREATE_IF_NOT_FOUND 先查找声明的查询,如不能找到,将生成一个基于命名的查询(默认查询策略,一般不用变)

查询语句

创建查询(去重、区间、忽略大小写等)

查询的构建机制,将截断前缀 find…By、 read…By、 query…By、 count…By、 get…By 等,从剩余部分开始解析,省略号中可以使用如:Distinct、Between、LessThan、GreaterThan、Like等表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface PersonRepository extends Repository<User, Long> {

List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

// 使用 Distinct 去重
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

// 使用 IgnoreCase 忽略大小写查询
List<Person> findByLastnameIgnoreCase(String lastname);
// 使用 AllIgnoreCase 全部忽略大小写
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

// 使用 OrderBy (Field) ASC/DESC 进行排序
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

属性表达式(子属性查询)

即一个被管理实体的属性,在查询时,会去查找该属性类的嵌套属性类。如:Person 有一个 Health 属性类,二Health 也有一个 HeartIm 属性类,则通过方法名查询为:

1
List<Person> findByHealthHeartIm(HeartIm heartIm);    // 相当于 {health.heart:?0}

其查询顺序为,先匹配 healthheartIm 属性是否存在,若否,匹配 healthHeart.Im ,最后才是 health.Heart.Im。再没有则接着向下拆分。为了解决模糊不清的含义,我们可以在方法名中使用 “_” 手动创建分割点。

1
List<Person> findByHealth_HeartIm(HeartIm heartIm);

特殊参数(分页、排序)

除了在查询中定义处理方法参数之外,还有一些特殊的类型,如:Pageable 和 Sort,用于分页和排序:

1
2
3
4
5
6
7
8
9
// Page 接口中返回了元素的总数、可分页数等,其实是通过底层触发 count 方法进行了总数查询
Page<User> findByLastname(String lastname, Pageable pageable);

// Slice 仅仅知道是否有下一个可用的 Slice,在遍历大结果集时非常有效
Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

限制查询结果(Top、First等)

查询方法的结果可以通过关键字:first、top 来限制,紧跟随的数值会限定长度,默认为1

1
2
3
4
5
6
7
8
9
10
11
User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

查询结果流(Stream)

查询的结果可以使用 java8 的 Stream 来处理,这样可以使用 stream 的良好性能。

1
2
3
4
5
6
7
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

因为 Stream 使用了底层的资源,所以在使用之后必须关闭:

1
2
3
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {  
stream.forEach(…);
}

而且,并不是所有的 Spring Data 模块都支持 Stream

异步查询结果

Repository 的查询方法可以异步执行,这意味着该方法在调用时会立即返回,但是 实际的查询要提交给 Spring 的任务TaskExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** Future 的常用方法: 
isCancelled():boolean
isDone():boolean
get():V
get(long timeout,@NotNull TimeUnit unit):V
*/
@Async
Future<User> findByFirstname(String firstname); // java.util.concurrent.Future

@Async
CompletableFuture<User> findOneByFirstname(String firstname); // Java 8 的 java.util.concurrent.CompletableFuture

@Async
ListenableFuture<User> findOneByLastname(String lastname); // org.springframework.util.concurrent.ListenableFuture

生成 Repository 实例

使用 xml 配置的方式 指定repositories 扫描的包路径:

1
<repositories base-package="com.acme.repositories" />

使用注解的方式:

1
2
3
4
5
6
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() { // … }
}

以上是 Spring Data 的公共基础部分,再往下就是 MongoDBFactory 等的底层实现了。才疏学浅,看不下去啊。就到这里吧,第八章。


自定义converter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.pgc.diagnose.config;

import com.mongodb.MongoClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.convert.CustomConversions;

import java.util.ArrayList;
import java.util.List;

@Configuration
//@EnableAutoConfiguration(exclude = {EmbeddedMongoAutoConfiguration.class})
//@Profile("!testing")
public class MongoConfig extends AbstractMongoConfiguration {

@Value("${spring.data.mongodb.uri}")
private String host;

@Override
public MongoClient mongoClient() {
return new MongoClient("127.0.0.1", 27017);
}

@Override
protected String getDatabaseName() {
return "ch_node";
}

@Override
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new StringToPointConverter2());
return new CustomConversions(converters);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.pgc.diagnose.config;

import com.pgc.common.exception.BadRequestException;
import com.pgc.diagnose.model.Point;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Set;

@Component
public class StringToPointConverter implements ConditionalGenericConverter {

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return sourceType.getType().equals(String.class) && targetType.getType().equals(Point.class);
}

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Point.class));
}

@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String from = (String) source;

if (from != null){
String[] strings = from.split("#");

if (strings.length == 0 || strings.length > 2)
throw new BadRequestException("String 转 Point 失败!");

if (strings.length == 1)
return Point.build(Point.Track.valueOf(strings[0]));

return Point.build(Point.Track.valueOf(strings[0]), Point.Industry.valueOf(strings[1]));
}

throw new ConverterNotFoundException(sourceType, targetType);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.pgc.diagnose.config;

import com.pgc.common.exception.BadRequestException;
import com.pgc.diagnose.model.Point;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
public class StringToPointConverter2 implements Converter<String, Point> {
@Override
public Point convert(String from) {

if (from != null){
String[] strings = from.split("#");

if (strings.length == 0 || strings.length > 2)
throw new BadRequestException("String 转 Point 失败!");

if (strings.length == 1)
return Point.build(Point.Track.valueOf(strings[0]));

return Point.build(Point.Track.valueOf(strings[0]), Point.Industry.valueOf(strings[1]));
}

return null;
}
}

1
2
3
4
5
6
7
8
9
10
package com.pgc.diagnose.config;

import com.pgc.common.config.WebConfig;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DiagnoseAppWebConfig extends WebConfig {

}