Spring Data Kay 预览

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

随着我们刚刚发布了 Spring Data Kay 发布列车第四个里程碑版本,让我们来看看自第一个里程碑版本以来,列车上的 13 个模块所带来的更改和功能。这篇博文涵盖了一系列更改,但绝不是对 M2 和 M4 之间 550 多个更改的全面概述。要获取更改的完整列表,请查看我们的 Jira,了解 Kay M1M2M3M4 的更改。

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

  • 响应式支持的调整

  • 可组合的存储库

  • 改进的 CRUD 方法命名方案

  • Fluent MongoOperations API

  • MongoDB 的 Template API 的 Kotlin 扩展

  • MongoDB 校对支持

  • Redis 客户端配置

  • Cassandra 轻量级事务支持和查询/更新对象

  • Java 9 兼容性

  • 升级到 Elasticsearch 5.4

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

响应式支持的更改

在我们去年年底为 Redis、Cassandra 和 MongoDB 的响应式数据访问提供了 重要的基础 之后,Couchbase 也加入了响应式阵营。Couchbase 驱动程序完全基于 RxJava 1 构建,因此响应式 Template API 使用 RxJava 1。Couchbase 存储库可以与 RxJava 1 和 2 以及 Reactive Stream 类型平滑地协同工作。

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

@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 的功能冻结之后,RxJava 1 将不再在 2018 年 3 月之后维护。因此,我们决定弃用我们的 RxJava 1 存储库,并在即将发布的候选版本中删除它们。请放心,RxJava 1 支持仍然有效,因此如果您需要 RxJava 1 存储库,可以将这些接口复制到您的项目代码中。

如果您是响应式 MongoDB 存储库的用户,您可能希望了解我们已将 @InfiniteStream 重命名为 @Tailable 以反映底层游标,尽管无限流听起来很不错。

可组合的存储库

Spring Data 存储库是实现 DDD 风格存储库的工具。它们的实现设计为包含三个主要部分:特定于存储库的基本实现、支持查询方法的查询执行引擎以及可选的自定义实现,用于将自定义功能链接到存储库实例。

随着时间的推移,我们发现最初的设计在存储库实现方面存在局限性。存储库可以通过 QueryByExampleExecutor 提供查询示例操作,或通过 QuerydslPredicateExecutor 提供 Querydsl 支持。这两者都是与特定于存储库的基本存储库实现正交的功能,并且需要一些技巧才能发挥作用。

情况类似于自定义实现部分:只支持单个自定义实现对象,这需要将您想要链接到存储库对象的所有功能都包含在内。

可组合的存储库通过将面向组合的方法转变为一等公民来消除上述设计限制。存储库不再局限于基本实现和自定义实现,而是由片段组成。片段表示一个接口以及一个实现对象。多个片段形成一个用于实现存储库的组合。

考虑以下存储库声明

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。该存储库由 CRUD 操作组成:通过 ContactFragment 进行 Contact 搜索,以及通过 PersonBatchExecutor 进行一些批量操作。每个接口存储库都是一个由实现支持的片段接口。

对存储库方法的调用会根据其方法实现进行路由。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 的角度来看,具有相同名称和签名的多个方法会被合并为单个方法,而不会进一步区分。

在实现方面,可能有多个可用的实现声明了相同的方法签名。可组合的存储库使用**接口声明顺序**来区分多个实现。

查看以下示例

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 是第一个声明的。因此,第二个声明将为 search(…) 方法的调用选择 PersonFragmentImpl

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

复合存储库是链接自定义实现片段的一种选择。它们提供了一种强大的方法来定义单个查询并将其挂钩,而不会丢失 Spring Data 的其余预构建选项。如果您之前使用过自定义实现功能,请放心,这些功能将按预期继续运行。

改进的 CRUD 存储库方法命名

在 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 之间添加的任何内容都会使您的方法名称更具表达性,并且不会影响查询派生。

Fluent 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 中的集合包含不同类型的实体。例如,JediSWCharacter 集合中。通过 ….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 命令有效的那些方法,将实体作为 GeoResult 提取到 GeoResults

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 Collation Support

MongoDB 3.4 引入了对 校对 的原生支持,允许指定 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 client configuration

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

我们将 Redis 客户端配置组织到特定于环境和客户端的部分。特定于环境的配置包含端点、数据库和身份验证(基于密码)详细信息

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

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

  • RedisClusterConfiguration - 用于 Redis 集群。

两个受支持的客户端 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。

Spring Data for Apache Cassandra

在此版本中,我们简化了模块的库布局。Spring Data for Apache Cassandra 过去附带两个库,Spring CQL 和 Spring Data Cassandra。使用 Spring Data Kay,我们将 spring-cql 合并到 spring-data-cassandra 中,因为大多数用法无论如何都在 spring-data-cassandra 中。通过此更改,我们已经应用了一些包重命名。我们将为即将发布的 RC1 版本完成这些操作。

Cassandra Query & Update objects

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、添加到列表、从集合中删除等)。

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 的更多详细信息。

Lightweight transactions

轻量级事务(比较并设置事务)通过其查询选项支持实体的 insertupdate 操作。操作结果通过返回实体或 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 compatibility

The problem

即将发布的 Java 9 版本的行为将与过去的版本有所不同。它将破坏许多不适应这些更改的现有应用程序。这是由 Java 平台模块系统 (JPMS) 和相关的 Java 内部封装引起的。应用程序可能会中断,原因可能有三个。

  • 非法使用内部 API。会遇到类似这样的异常:

    模块未将 "<package.abc>" 打开到未命名模块 @

  • 内部 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 9 兼容的版本,该版本具有与 Java 8 兼容版本相同的性能特性(有关详细信息,请参阅 DATACMNS-1080)。

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

所有相关人员都希望使迁移到 Java 9 尽可能顺利,您可以真正帮助实现这一目标。获取 JDK 9 的早期访问版本,并使用它构建和运行您的应用程序。如果您遇到问题,请在您遇到问题的项目中提交问题。

未命名模块

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

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

升级到 Elasticsearch 5.4

我们使用传输客户端将 Spring Data Elasticsearch(感谢 Moshin 和 Artur!)升级到了 5.4。

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

总结

从 Spring Data Kay 的第一个里程碑开始,这条路已经很长了,我们希望您对发布列车将发布的内容有了一个很好的印象。我们将为即将发布的候选版本添加一些小功能,并在 8 月初短暂发布 GA 版本。如果您有任何问题、反馈或想与我们讨论功能,请通过 JiraStack OverflowTwitterGitter 联系我们。

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部