前言
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);
}
|
除了 CrudRepository 之外,还有 JpaRepository 和 MongoRepository。在 CrudRepository 中,有许多抽象方法添加了额外的方法来简化对实体的分页访问。
1 2 3 4 5 6 7 8 9 10
|
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>{ 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> { … }
|
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> { … }
|
定义查询方法
仓库代理有两种方式导出指定的查询。
- 从名字直接导出查询 : 类似 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);
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
List<Person> findByLastnameIgnoreCase(String lastname); List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname); }
|
属性表达式(子属性查询)
即一个被管理实体的属性,在查询时,会去查找该属性类的嵌套属性类。如:Person 有一个 Health 属性类,二Health 也有一个 HeartIm 属性类,则通过方法名查询为:
1
| List<Person> findByHealthHeartIm(HeartIm heartIm);
|
其查询顺序为,先匹配 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<User> findByLastname(String lastname, Pageable pageable);
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
|
@Async Future<User> findByFirstname(String firstname);
@Async CompletableFuture<User> findOneByFirstname(String firstname);
@Async ListenableFuture<User> findOneByLastname(String lastname);
|
生成 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
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 {
}
|