Spring Data Hopper 有什么新功能?

工程 | Christoph Strobl | 2016 年 5 月 3 日 | ...

正如我们刚刚发布了 Spring Data Hopper 版本列车的 GA 版本一样,让我们更深入地了解该列车包含的 13 个模块所带来的变化和功能。该版本列车依赖项的一个非常基础的变化是将 Spring Framework 升级到 4.2 版本(目前为 4.2.5)作为基线。这是为即将发布的 Framework 4.3 版本做准备。我们还借此机会将 Querydsl 集成升级到 4.x 版本(目前为 4.1),这在一些核心抽象中引入了一些破坏性变更。除此之外,Hopper 在其模块中包含了相当多重大的主要版本变化。

这些升级主要是由底层存储驱动和实现的重大版本更新驱动的,这些更新需要在这些模块暴露的 API 中体现为潜在的破坏性变更。其中一些模块——例如 Spring Data Neo4j 和 Spring Data Couchbase——已经在版本列车之外发布了新的主要版本,现在又重新整合到其中。

欢迎(回归)

  • 基于 Couchbase 2.2 的 Spring Data Couchbase 2.1
  • 基于 Elasticsearch 2.2 的 Spring Data Elasticsearch 2.0
  • 基于 Neo4J OGM 2.0 的 Spring Data Neo4j 4.1
  • 基于 Solr 5.5 的 Spring Data Solr 2.0

除了这些升级,团队还在开发许多新功能。

  • 使用 @AliasFor 的可组合注解
  • Query By Example(示例查询)
  • 仓库查询方法的 Projection(投影)
  • Redis 集群和仓库支持
  • MongoDB 的 $lookup 聚合和批量操作
  • 在 Gemfire 和 Redis 中使用 Spring 4.3 时的同步缓存查找
  • Querydsl 4 支持

我想在本文的其余部分对其中一些功能做更详细的阐述,以便为您提供更详细的概览。

Query By Example(示例查询)

Spring Data 仓库抽象允许通过 Querydsl 执行查询方法和灵活的谓词已经有一段时间了。话虽如此,提供一个部分设置好的域类型实例作为探测器(probe)给仓库以返回与该特定探测器匹配的所有实体,这是一个长期以来备受期待的功能。Hopper 在 Spring Data Commons 中引入了对这种 Query-by-Example(示例查询)机制的通用支持,并在 JPA 和 MongoDB 模块中实现了该 API(更多功能将陆续推出)。

Query-by-Example API 由四个基本部分组成

  • 探测器实例,它是域模型的一个实例,其字段仅部分填充。
  • 可选的 ExampleMatcher,其中包含有关如何匹配特定字段、null 值、通用 String 等的详细信息和策略。
  • Example,由探测器和 ExampleMatcher 组成。
  • QueryByExampleExecutor 接口,您的仓库将额外实现该接口,该接口提供接受 Example 的方法,类似于 QueryDslPredicateExecutor

在最简单的情况下,只需在域类型上设置您想要查询的值,并将该示例交给仓库即可。

interface PersonRepository extends CrudRepository<Person, Long>,
  QueryByExampleExecutor<Person> { … }

Example<Person> example = Example.of(new Person("Jon", "Snow"));
Iterable<Person> result = repository.findAll(example);

默认情况下,给定值会原样匹配,null 值在查询创建期间会被忽略。您可以通过提供 ExampleMatcher 来自定义通用或单个字段的处理方式,从而获得对匹配过程的更多控制。

ExampleMatcher exampleSpec = new ExampleMatcher()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());

上述规范在上面的示例中为 JPA 创建了谓词,例如 like(firstname, "%Jon")like(lower(expression), "snow%")

还有更多选项可用。因此,请查阅 Spring Data JPAMongoDB 的参考文档。

仓库查询方法的 Projection(投影)

Spring Data REST 在 Evans 版本列车中引入了一个称为 Projection(投影)的功能。在 Hopper 版本中,我们为 JPA 和 MongoDB 查询方法添加了在仓库级别使用 Projection 的支持。Projection 是您的域模型的自定义视图,在这种情况下,它从查询方法返回。

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
}

@Entity
public class Address {

  @Id @GeneratedValue
  private Long id;
  private String street, state, country;
}

限制 Person 的数据暴露(仅暴露 firstNamelastName)通常需要一个专门的 DTO 类。使用 Projection,您只需定义一个接口,其中包含您想要暴露的属性(getter 方法),并将该 Projection 接口用作查询方法的返回类型。

interface NoAddresses {

  String getFirstName();

  String getLastName();
}

interface PersonRepository extends CrudRepository<Person, Long> {
  NoAddresses findByFirstName(String firstName);
}

Projection 是一种强大的模式,可以从现有模型构建调整后的视图。NoAddresses 是一个封闭式 Projection,因为它不包含任何动态计算值的方法(更多内容请参阅下文)。封闭式 Projection 使我们能够优化查询执行,因为只从数据存储中查询暴露的属性。因此,在上述情况下,实际执行的查询在语义上等同于 select u.firstName, u.lastName from User u where u.firstName = ?1。返回的元组然后被包装到一个代理中,该代理返回与声明的访问器对应的值。

然而,Projection 也可以用于丰富数据模型。您可以使用 SpEL 表达式通过 @Value 注解暴露的属性来暴露合成属性。

interface FullNameAndCountry {

  String getFirstName();

  String getLastName();

  @Value("#{target.firstName} #{target.lastName}")
  String getFullName();

  @Value("#{target.address.country}")
  String getCountry();

  @Value("#{@mybean.someMethod(target)}")
  String getSomeCalculatedValue()
}

请注意,我们可以如何利用目标实例的属性,遍历未在顶层暴露的嵌套属性,甚至调用其他 Spring bean 上的方法并将目标传递给它以用于高级计算。在这种情况下,不会应用查询优化,因为为该接口创建的代理将需要访问原始目标实例。

有关如何在查询方法中使用 Projection 的更多详细信息,请参阅 Spring Data JPASpring Data MongoDB 的参考文档。

Redis 集群

对 Redis 集群的支持在现有的具有集群功能的 Redis 驱动程序之上提供了一个高级 API。集群支持基于与非集群通信完全相同的构建块。RedisClusterConnectionRedisConnection 的扩展,负责处理与 Redis 集群的通信,并将错误转换为 Spring 的 DataAccessException 层次结构。您会发现,与您在使用 Spring Data Redis 时已经习惯的方式没有太大区别。

Redis 集群的行为与单节点 Redis 甚至 Sentinel 监控的主从环境不同。这是由于自动分片将一个键映射到 16384 个槽中的一个,这些槽分布在各个节点上。因此,涉及多个键的命令必须断言所有键都映射到完全相同的槽,以避免跨槽执行错误。

RedisClusterConnection 既提供了与单个槽或节点通信的 API,又在与集群交互时保持了预期的行为。它负责执行涉及多个键、槽或集群节点的命令,因此会连接到所需的节点并收集结果,以便例如 KEYS 命令不仅返回单个节点的匹配键,还会返回集群内所有匹配键的累积列表。

更多信息以及如何设置 Spring Data Redis 以与集群一起工作的完整示例可以在 Spring Data Examples GitHub 仓库的 Redis Cluster 模块以及参考文档中找到。

Redis 仓库

借助 Redis 仓库,Hopper 在 Redis 之上提供了一个 Spring Data 仓库抽象的实现,以便您可以执行基本的 CRUD 操作和派生查询方法。它允许您无缝地将域对象转换并存储到 Redis 哈希中,应用自定义映射策略并利用二级索引。让我们看看一个示例域类型、一个仓库以及引导 Redis 仓库所需的 Spring 配置。

@RedisHash("persons")
class Person {

  @Id String id;
  String firstname;
  @Indexed String lastname;
  Address address;
}

interface PersonRepository extends CrudRepository<Person, String> {
  List<Person> findByLastname(String lastname);
}

@Configuration
@EnableRedisRepositories
class ApplicationConfig {

  @Bean
  RedisConnectionFactory connectionFactory() {
    return new JedisConnectionFactory();
  }

  @Bean
  RedisTemplate<?, ?> redisTemplate() {
    return new RedisTemplate<byte[], byte[]>(connectionFactory());
  }
}

请注意,用于 firstname@Indexed 注解允许在派生查询方法中使用该属性。这也适用于嵌套或嵌入对象。有关自定义对象映射策略、过期时间、监听器以及存储对象引用的更多信息,请参阅 Spring Data Redis 参考文档

Spring Data REST

Spring Data REST 的一个常见请求是使用聚合根的唯一属性值来构成暴露的项资源的 URI。想象一个非常简单的实体 Country,您希望在 URI 中使用其唯一名称。

@Entity
class Country {

  @Id @GeneratedValue
  private Long id;
  private String name;
}

为了实现这一点,Spring Data REST 的 RepositoryRestConfiguration 现在允许通过专门的 API 定制实体查找。为 URI 使用不同的属性实际上需要定义两件事:要使用的属性以及仓库上的一个查询方法,用于将该属性的值映射回该属性的实例。如果您使用 Java 8,注册就像这样简单:

@Component
public class SpringDataRestCustomization extends RepositoryRestConfigurerAdapter {

  @Override
  public void configureRepositoryRestConfiguration(
    RepositoryRestConfiguration config) {

    config.withCustomEntityLookup().
      forRepository(CountryRepository.class,
        Country::getName, CountryRepository::findByName);
  }
}

正如您所看到的,我们在这里使用方法句柄来定义双向映射步骤,以便基础设施能够识别。当然,这里显示的方法也有替代的重载版本,也适用于 Java 6。有关详细信息,请务必查看 GitHub 上专门针对此问题的示例

Lookup 类型

通常,域模型包含一些类型,它们是值对象,但实际上代表了特定可能值集合中的一个特定值。上面示例中的 Country 类实际上属于这一类别。由于我们需要管理这些值的超集,因此存在一个仓库。如果也允许通过 REST 管理该集合,那么该仓库也需要导出。由于仓库通常表示正在管理的聚合,Spring Data REST 处理这种情况的默认方式是在遇到 Country 实例的地方渲染到关联资源的链接。Hopper 版本列车添加了声明所谓的 lookup 类型的方法,对于这些类型,Spring Data REST 会在表示中内联渲染一个独立属性,并注册相应的 Jackson 反序列化器,以确保该属性值在处理 PUTPOST 请求时被转换回该值类型的实例。

假设包含 Country 实例的资源的原始表示如下:

{
  "zipCode" : "…",
  "_links" {
    "country" : { "href" : "…" }
  }
}

如果您现在将 Country 注册为 lookup 类型,如下所示:

@Component
public class SpringDataRestCustomization extends RepositoryRestConfigurerAdapter {

  @Override
  public void configureRepositoryRestConfiguration(
    RepositoryRestConfiguration config) {

    config.withCustomEntityLookup().
      forLookupRepository(CountryRepository.class).
      withIdMapping(Country::getName).
      withLookup(CountryRepository::findByName);
  }
}

表示将更改为如下所示,但在模型中仍然保持实体语义:

{
  "zipCode" : "…",
  "country" : "Germany"
}

可组合注解

升级到 Spring 4.2 作为框架基线使我们能够提供增强的选项来组合您自己的注解。我们将基础架构添加到了 Spring Data Commons 模块,并调整了 JPA、MongoDB 和 Redis 的实现,以便您可以使用这些更改。

假设您在许多地方一起使用 Spring Data JPA 注解 @Modifying@Query,就像这样:

@Modifying
@Query("update #{#entityName} u set u.active = ?1 where u.id in ?2")
void updateUserActiveState(boolean activeState, Integer... ids);
@Modifying
@Query
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyingQuery {

  @AliasFor(annotation = Query.class, attribute = "value")
  public String query();
}

@ModifyingQuery(query =
  "update #{#entityName} u set u.active = ?1 where u.id in ?2")
void updateUserActiveState(boolean activeState, Integer... ids);

或者只是稍微娱乐一下,将现有注解翻译成您喜欢的语言

Spring Data 团队将来到拉斯维加斯!务必参加今年 SpringOne Platform 上关于 Spring Data 的众多会议。第一批特色演讲已在该活动的网站上发布。请务必注册,了解 Spring Data、整个 Spring Framework 生态系统以及所有与 CloudFoundry 相关内容的最新和最重要进展。

订阅 Spring 资讯

通过 Spring 资讯保持联系

订阅

领先一步

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

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部