Spring Data Kay 预览

工程 | Mark Paluch | 2017年6月20日 | ...

随着 Spring Data Kay 发布列车的第四个里程碑版本刚刚发布,让我们回顾一下自第一个里程碑以来,该列车上 13 个模块带来的变化和功能。这篇博客文章涵盖了一些变化,但绝不是 M2 和 M4 之间 550 多项变化的全部。要获取完整的更改列表,请查看我们在 Jira 中关于 Kay M1M2M3M4 的更改。

以下是我们精选的关键变更列表

  • 响应式支持的调整

  • 可组合的 Repository

  • CRUD 方法改进的命名方案

  • 流畅的 MongoOperations API

  • MongoDB Template API 的 Kotlin 扩展

  • MongoDB 排序规则支持 (Collation Support)

  • Redis 客户端配置

  • Cassandra 轻量级事务支持和 Query/Update 对象

  • Java 9 兼容性

  • 升级到 Elasticsearch 5.4

我们非常乐意听取您的反馈,您可以在本文末尾找到与我们联系的方式。

响应式支持的变更

去年底,在我们为 Redis、Cassandra 和 MongoDB 的响应式数据访问奠定了重要基础并提出初步草案后,Couchbase 也加入了响应式阵营。Couchbase 驱动程序是完全响应式的,基于 RxJava 1 构建,因此响应式 Template API 使用 RxJava 1。Couchbase Repository 可以顺利地与 RxJava 1 & 2 以及 Reactive Stream 类型协同工作。

同时,我们通过预定义的 RxJava2CrudRepositoryRxJava2SortingRepository 接口添加了对 RxJava 2 类型的支持,因此您可以在所有响应式 Repository(Apache Cassandra、Couchbase 和 MongoDB)中使用 RxJava 2 类型。

@Repository
interface RxJava2PersonRepostitory extends
                    RxJava2SortingRepository<Person, String> {

  Flowable<Person> findByFirstnameAndLastname(String firstname, String lastname);

  Maybe<Person> findByLastname(String lastname);

  Single<Person> findProjectedByLastname(Maybe<String> lastname);

  Observable<ProjectedPerson> findProjectedByLastname(Single<String> lastname);
}

Spring Data 将在未来几个月内发布 GA(通用版本)。这将是在 RxJava 1 功能冻结(2018 年 3 月之后不再维护)之后。因此,我们决定弃用 RxJava 1 Repository,并在即将发布的候选版本中将其移除。请放心,RxJava 1 支持仍然可用,如果您需要 RxJava 1 Repository,可以将这些接口复制到您的项目代码中。

如果您是响应式 MongoDB Repository 的用户,您可能会很高兴得知我们将 @InfiniteStream 重命名为 @Tailable,以反映底层的游标,尽管“无限流”听起来很花哨。

可组合的 Repository

Spring Data Repository 是一个实现 DDD (领域驱动设计) 风格 Repository 的工具。它的实现被设计为包含三个主要部分:特定于存储的基础实现、支持查询方法的查询执行引擎,以及一个可选的自定义实现,用于将定制功能链接到 Repository 实例中。

随着时间的推移,我们发现我们最初的设计在 Repository 实现方面存在局限性。一个 Repository 可以通过 QueryByExampleExecutor 提供 Query-by-Example 操作,或者通过 QuerydslPredicateExecutor 提供 Querydsl 支持。这两者都是特定于存储的基础 Repository 实现的补充特性,需要一些技巧才能实现。

这种情况类似于自定义实现部分:只支持单个自定义实现对象,这需要将所有您想链接到 Repository 对象的功能都集中在一起。

可组合 Repository 通过将组合导向的方法提升为一等公民,消除了上述设计限制。Repository 不再局限于基础实现和自定义实现,而是由片段组成。一个片段代表一个接口以及一个实现对象。多个片段组成一个用于实现 Repository 的组合。

考虑以下 Repository 声明

class Person extends Contact {
  // …
}

interface PersonRepository extends CrudRepository<Person, String>,
                                      ContactFragment,
                                      PersonBatchExecutor {
}

interface ContactFragment {
  Iterable<Contact> search(ContactQuery query);
}

interface PersonBatchExecutor {
  void bulkEdit(PersonQuery query, PersonUpdate update);
}

上面,我们有一个简单的领域对象 Person,它也是一个 Contact。该 Repository 由 CRUD 操作组成:通过 ContactFragment 进行 Contact 搜索,以及通过 PersonBatchExecutor 进行一些批量操作。每个接口 Repository 都是一个由实现支持的片段接口。

对 Repository 方法的调用根据其方法实现进行路由。CRUD 操作路由到特定于存储的实现,而对 search(ContactQuery query) 的调用则路由到用户提供的 ContactFragment 实现(如下所示)。

在启动时,配置组件会扫描已声明片段接口的实现。

interface ContactFragment {
  Iterable<Contact> search(ContactQuery query);
}

class ContactFragmentImpl implements ContactFragment {

  @Override
  Iterable<Contact> search(ContactQuery query) {
    // …
  }
}

片段实现是功能齐全的 Spring bean。它们通过片段接口名称加上实现后缀来按名称查找,例如,默认情况下,ContactFragment 的实现将被查找为 ContactFragmentImpl

重载方法的顺序

Java 允许组合多次声明相同方法的接口。从 API 的角度来看,多个具有相同名称和签名的方法会被合并成一个方法,不做进一步区分。

在实现方面,可能存在声明相同方法签名的多个可用实现。可组合 Repository 使用接口声明顺序来消除多个实现之间的歧义。

请看下面的例子

public interface ContactFragment {
  Iterable<Contact> search(ContactQuery query);
}

public interface PersonFragment {
  Iterable<Contact> search(ContactQuery query);
}

// Calls search(…) on ContactFragment
public interface PersonRepository implements CrudRepository<Person, String>,
  ContactFragment, PersonFragment {
  …
}

// Calls search(…) on PersonFragment
public interface PersonRepository implements CrudRepository<Person, String>,
  PersonFragment, ContactFragment {
  …
}

PersonRepository 的第一次声明将首先在 ContactFragmentImpl 上调用 search(…) 方法,因为 ContactFragment 是首先声明的。因此,第二次声明将选择 PersonFragmentImpl 来调用 search(…) 方法。

如果没有任何片段提供实现,则特定于存储的方面(如 Querydsl)和基础实现将作为备选方案。

复合 Repository 是链接自定义实现片段的一种选择。它们提供了一种强大的方式来定义单个查询并将其集成进去,而不会丢失 Spring Data 其他预构建的选项。如果您之前使用过自定义实现功能,请放心,这些功能将继续按预期工作。

CRUD Repository 方法改进的命名

在第一代 Spring Data 中,CrudRepository 中的方法命名方案引起了一些问题。特别是那些接受泛型类型变量作为参数的方法。在某些情况下(例如领域或标识符类型实现了 Iterable),它们实际上可以解析为相同的方法,并导致由 save(…)delete(…) 引起歧义。

随着 Kay 版本的发布,我们决定根据以下原则重命名方法:

  1. 能够根据名称和(原始)参数类型查找方法。

  2. 命名为 …All(…) 的方法影响一组项和/或返回一个集合。

  3. 接受标识符的方法命名为 …ById(…)

  4. 让我们放弃 ID extends Serializable 的要求。

方法重命名后,我们不再要求标识符实现 Serializable。这是由 JPA 的标识符处理引入的约束,当时 Spring Data 仅与 JPA 一起使用。我们的 NoSql 存储不强制要求可序列化标识符,因此取消此要求可减少许多地方的复杂性。

新的 CrudRepository 采用了统一的命名方案,不会导致解析歧义。

interface CrudRepository<T, ID> extends Repository<T, ID> {

    S save(S entity);

    Iterable<S> saveAll(Iterable<S> entities)

    Optional<T> findById(ID id);

    boolean existsById(ID id);

    Iterable<T> findAllById(Iterable<ID> ids);

    void deleteById(ID id);

    void delete(T entity);

    void deleteAll(Iterable<? extends T> entities);
}

然而,如果您之前有一个名为 id 的属性,但它不是实体标识符怎么办?为此属性编写的查询方法不会与 findById(…) 和其他 …ById(…)` 方法冲突吗?会的。

如果您遇到这种情况,可以在方法名称中插入自定义区分,例如,如果您处理 Person 实体,可以使用 findPersonById(…)。Spring Data 的方法解析使用前缀关键字,如 findexistscountdelete,以及一个终止关键字 By。您在 findBy 之间放置的任何内容都会使您的方法名称更具表达性,并且不会影响查询派生。

流畅的 MongoOperations API

MongoOperations 接口是与 MongoDB 进行更底层交互时的核心组件之一。它提供了广泛的方法,涵盖了从集合/索引创建和 CRUD 操作到更高级功能(如 map-reduce 和聚合)的需求。

查看 MongoOperations,您会发现每个方法都有多个重载。其中大多数仅涵盖 API 的可选/可空部分。虽然这可能非常方便,但也变得冗长,几乎到了难以阅读的地步。

// ...excerpt from MongoOperations

<T> List<T> find(Query query, Class<T> entityClass);
<T> List<T> find(Query query, Class<T> entityClass, String collectionName);

<T> T findOne(Query query, Class<T> entityClass);
<T> T findOne(Query query, Class<T> entityClass, String collectionName);

<T> CloseableIterator<T> stream(Query query, Class<T> entityType);
<T> CloseableIterator<T> stream(Query query, Class<T> entityType, String collectionName);

通过 FluentMongoOperations,我们引入了一个专门为 MongoOperations 的常用方法量身定制的接口,提供了一种更易读、更流畅的 API。入口点 insert(…)find(…)update(…) 等遵循基于执行操作的自然命名方案。从入口点开始,API 被设计为仅提供依赖于上下文的方法,引导至调用实际 MongoOperations 对应方法的终止方法。

让我们看一个具体的例子。假设您有一个星球大战角色的集合,其中包含 Jedi。在经典的 MongoOperations 风格中,查找该集合中所有实体的方式如下所示:

Query query = new BasicQuery(new Document());
List<SWCharacter> all = ops.find(query, SWCharacter.class, "star-wars");

使用 FluentMongoOperations,上述内容可以表示为

List<SWCharacter> all = ops.find(SWCharacter.class)
  .inCollection("star-wars")
  .all();

如果 SWCharacter 通过 @Document 定义了集合,或者您使用类名作为集合名称,则可以跳过 ….inCollection("star-wars") 步骤,如下所示。

List<SWCharacter> all = ops.find(SWCharacter.class).all();

有时,MongoDB 中的一个集合包含不同类型的实体。例如,在 SWCharacter 集合中的一个 Jedi。通过 ….as(Jedi.class) 细化请求将导致查询结果映射到 Jedi

Optional<Jedi> luke = ops.find(SWCharacter.class)
  .as(Jedi.class)
  .matching(query(where("firstname").is("luke")))
  .one();

在检索单个实体、作为 ListStream 的多个实体之间切换是通过终止方法 first()one()all()stream() 完成的。

通过 near(NearQuery) 编写地理空间查询时,终止方法的数量会更改为仅限在 MongoDB 中执行 geoNear 命令时有效的方法,这些方法将在 GeoResults 中以 GeoResult 的形式获取实体。

GeoResults<Jedi> results = mongoOps.query(SWCharacter.class)
  .as(Jedi.class)
  .near(alderaan) // NearQuery.near(-73.9667, 40.78).maxDis…
  .all();

同样的工作方式也适用于 FluentMongoOperations 的其他 API 部分。

ops.update(Jedi.class)
  .matching(query(where("lastname").is("solo")))
  .apply(update("firstname", "han"))
  .upsert();

ops.remove(SWCharacter.class)
  .matching(query(where("name").is("yoda")))
  .all();

ops.aggregateAndReturn(Jedi.class)
  .by(newAggregation(Person.class, project("firstna...
  .all();

MongoDB 排序规则支持

MongoDB 3.4 引入了对排序规则 (Collations) 的原生支持,允许为 String 比较指定特定于语言的规则。现在,排序规则可以在大多数 MongoDB 命令中使用,例如在创建集合或索引时,以及用于 queryfindAndModifyremove 和其他操作。

值得一提的是,从 MongoDB 3.4 开始,现在可以使用不同的排序规则为相同的字段设置多个索引。这对于 MongoDB 本身的查询计划非常重要,因为只有定义相同排序规则的查询才能实际利用该索引。

CollectionOptions collectionOptions = CollectionOptions.empty()
  .collation(Collation.of("en_US")
     .strength(primary().includeCase()));

template.createCollection("persons", collectionOptions);

IndexDefinition index = new Index()
  .named("en-name-idx")
  .on("name", Direction.ASC)
  .collation(Collation.of("en").caseFirst(off()));

template.indexOps("persons").ensureIndex(index);

如果未提供排序规则,MongoDB 将使用简单的二进制比较,也可以通过 Collation.simple() 显式设置。为了在整个 Spring Data MongoDB 中使用 Collation 支持,我们为 QueryNearQueryAggregationOptions 等引入了各种扩展点。

Query query = query(where("firstName").is("Amél"))
  .collation(collation);

NearQuery nearQuery = near(-73.9667, 40.78)
  .query(where(…))
  .collation(Collation.of("fr")));

AggregationOptions options = new AggregationOptions.Builder()
  .collation(Collation.of("en_US"))
  .build();

在撰写本文时,尚不支持通过 @Indexed 定义 Collation。展望未来,我们将利用 Java 8 的可重复注解添加此功能。

Redis 客户端配置

如果您考虑 Redis 的各种操作模式(Standalone、Sentinel、Cluster),Spring Data Redis 连接工厂的配置可能会很麻烦。使用特定于客户端的方面(SSL 支持、连接池等)需要额外的配置对象或条件配置。

我们将 Redis 客户端配置分为环境相关和客户端相关的部分。环境相关配置包含端点、数据库和身份验证(基于密码)详情:

  • RedisStandaloneConfiguration - 用于 Redis Standalone。如果使用复制,您也可以用它连接到特定的 Redis 主节点或从节点。

  • RedisSentinelConfiguration - 当您的 Redis 节点由 Redis Sentinel 管理时使用。

  • RedisClusterConfiguration - 用于 Redis Cluster。

Lettuce 和 Jedis 是两个受支持的客户端,每个客户端都使用各自特定的配置进行配置:LettuceClientConfigurationJedisClientConfiguration。我们引入这种划分是因为每个客户端独立发展,共同点很少。

RedisStandaloneConfiguration envConfig =
  new RedisStandaloneConfiguration("localhost", 6379);
envConfig.setDatabase(2);
envConfig.setPassword(RedisPassword.of("foobared"));

LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
        .clientResources(…)
        .clientOptions(…)
        .commandTimeout(Duration.ofMillis(500))
        .shutdownTimeout(Duration.ofMillis(200))
        .useSsl().disablePeerVerification()
        .build();

connectionFactory = new LettuceConnectionFactory(envConfig, clientConfig);

RedisStandaloneConfiguration envConfig =
  new RedisStandaloneConfiguration("localhost", 6379);
envConfig.setDatabase(2);
envConfig.setPassword(RedisPassword.of("foobared"));

JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
        .clientName(environment.getProperty("spring.application.name"))
        .connectTimeout(Duration.ofMillis(200))
        .readTimeout(Duration.ofMillis(500))
        .useSsl().sslParameters(…).and()
        .usePooling().poolConfig(…)
        .build();

connectionFactory = new JedisConnectionFactory(envConfig, clientConfig);

以前,有些属性设置在连接工厂上,有些设置在特定于客户端的对象上。客户端配置是不可变的。您仍然可以在没有客户端配置的情况下使用连接工厂,但我们已弃用配置 setter。

Apache Cassandra 的 Spring Data

在此版本中,我们简化了模块的库布局。以前,Apache Cassandra 的 Spring Data 附带两个库:Spring CQL 和 Spring Data Cassandra。在 Spring Data Kay 中,我们将 spring-cql 合并到 spring-data-cassandra 中,因为大部分用法都在 spring-data-cassandra 内部。随此更改,我们已经应用了一些包重命名。我们将在即将到来的 RC1 版本中完成这些重命名。

Cassandra Query & Update 对象

QueryUpdate 对象允许对查询谓词和选择性更新进行细粒度控制。之前,我们支持通过更新所有属性来持久化整行,或者支持基于 CQL 的更新而不进行映射。

QueryUpdate 现在使用实体模型中的细节来支持属性到 Cassandra 列的映射。我们的查询映射器在查询执行之前转换值(映射的 UDT、特定数据类型的转换),因此您无需自行将查询值转换为特定于 Cassandra 的表示形式。

class Person {

  @PrimaryKeyColumn(name="last_name", ordinal = 0, type = PARTITIONED)
  String lastname;

  @PrimaryKeyColumn(name="firs_tname", ordinal = 1, type = CLUSTERED)
  String firstname;

  List<String> episodes;

  String mood;
}

Query query = Query.query(Criteria.where("lastname").is("White"))
  .and(Criteria.where("firstname").in("Walter", "Skyler"))
  .sort(Sort.by("firstname").ascending())
  .withAllowFiltering()
  .limit(10);

List<Person> people = cassandraOperations.select(query, Person.class);

Query 包含过滤条件、排序和一组查询选项来控制查询执行。您可以将其与 Template API 中的各种 selectupdate 方法一起使用,以查询数据进行选择或限制更新选择。

说到 Update,它允许您指定一组更新赋值。Update 支持 Apache Cassandra 3.10 中的所有更新运算符(set、add to list、remove from collection 等)。

Update update = Update.update("mood", "Bad")
  .addTo("episodes").appendAll("S1E1", "S1E2");

Query query = Query.query(Criteria.where("lastname").is("White"))
  .and(Criteria.where("firstname").is("Skyler"))
  .queryOptions(WriteOptions.builder().ttl(100).build());

cassandraOperations.update(query, update, Person.class);

有关 QueryUpdate 的更多详细信息,请参阅参考文档

轻量级事务

通过实体查询选项,insertupdate 操作支持轻量级事务(compare-and-set 事务)。操作结果通过返回实体或 null 来报告事务是否已应用。

InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build();

User user = new User("heisenberg", "Walter", "White");
User inserted = template.insert(user, lwtOptions);
User second = template.insert(user, lwtOptions); // returns null

我们对基本轻量级事务的支持随此版本发布,我们期待您的反馈。

Java 9 兼容性

问题

即将发布的 Java 9 版本将与以往版本有所不同。它将破坏许多未适应这些变化的现有应用程序。这是由 Java 平台模块系统 (JPMS) 以及相关的 Java 内部封装引起的。应用程序可能中断有三个潜在原因:

  • 非法使用内部 API。可能会收到类似如下的异常:

    moduledoes not "open <package.abc>" to unnamed module @

  • 内部 API 的行为改变(或内部 API 被移除)。

  • 默认情况下,未命名的模块只能访问 java.base 模块。

这在许多情况下是足够的,但不幸的是并非全部如此。因此,您可能需要使用 --add-modules <module name> 指定额外的模块作为依赖项。这里有一个 Java 本身定义的所有模块及其内容的列表,这将有助于找到正确的模块名称使用。

Spring Data 兼容性

我们很高兴地宣布 Spring Data 在 Java 9 上表现良好!

团队进行了修改,以确保我们的代码可以在 Java 9 上运行,而无需任何 --permit-illegal-access 命令行参数,该参数是解决第一个要点中提到的问题所必需的。我们也不使用适用于第二个要点的任何 API。

不幸的是,这种兼容性存在一些注意事项:

  1. 它不包括一些存储。我们在 DATACMNS-1033 中跟踪我们所了解的关于它们各自兼容性的信息。

  2. 您可能仍然会遇到第三个要点的问题。一个已知的例子是这样的异常:

    java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
    

    这可以通过在调用 java 时添加 --add-modules java.xml.bind 来解决。

  3. 目前,我们无法使用生成的属性访问器(在 Ingalls 发布列车中引入),因为生成的属性访问器不能再注入到原始类加载器中。这会导致轻微的性能下降。我们仍在寻找与 Java 8 兼容版本具有相同性能特征的 Java 9 兼容版本(详情请参阅 DATACMNS-1080)。

  4. 编译和执行测试仍然需要 --permit-illegal-access

所有参与者都希望使迁移到 Java 9 尽可能顺利,而您可以对此提供很大的帮助。获取 JDK 9 的早期访问版本,并使用它构建和运行您的应用程序。如果您遇到问题,请向您遇到问题的项目提交一个问题。

未命名的模块

如果您已经研究过 Jigsaw 项目,您可能注意到 Spring Data 没有合适的模块描述符,即它是一个所谓的未命名模块。这有一个主要缺点,就是我们无法在模块定义中指定模块依赖项,而必须在执行时使用 --add-modules 命令行选项提供它们。

问题在于我们所依赖的所有库都需要提供模块描述符。而且虽然我们的必需依赖项非常少,但可选依赖项却很多。只有当所有这些库都提供合适的模块后,我们才能开始自己提供合适的模块。

升级到 Elasticsearch 5.4

我们将 Spring Data Elasticsearch 升级到 5.4 版本(感谢 Moshin 和 Artur!),使用了 transport client。

这次升级需要在我们的公共 API 中进行一些更改。Template API 中的 scan 方法被替换为返回分页结果的 scroll 方法,并且我们将注解 (@CompletionField, @Field, @GeoPointField, @InnerField) 与 Elasticsearch 的 API 对齐。

总结

自 Spring Data Kay 的第一个里程碑以来,我们走过了一段漫长的道路,希望您对即将发布的列车内容有了很好的印象。我们将在即将发布的候选版本中添加一些小功能,并在 8 月初快速发布 GA 版本。如果您有问题、反馈或想与我们讨论功能,请通过 JiraStack OverflowTwitterGitter 与我们联系。

订阅 Spring 简报

订阅 Spring 简报,保持联系

订阅

保持领先

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring 通过简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中即将举行的所有活动。

查看全部