159. Spring Data Cloud Spanner

Spring Data是用于以多种存储技术存储和检索 POJO 的抽象。 Spring Cloud GCP 增加了对Google Cloud Spanner的 Spring Data 支持。

Maven 仅使用 Spring Cloud GCP BOM 协调此模块:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-gcp-data-spanner</artifactId>
</dependency>

Gradle coordinates:

dependencies {
    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner'
}

我们提供了一个用于 Spring Data Spanner 的 Spring Boot Starter,您可以利用它推荐的自动配置设置。要使用启动器,请参见下面的坐标。

Maven:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-data-spanner</artifactId>
</dependency>

Gradle:

dependencies {
    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner'
}

此设置还负责引入 Cloud Java Cloud Spanner 库的最新兼容版本。

159.1 Configuration

要设置 Spring Data Cloud Spanner,您必须配置以下内容:

  • 设置与 Google Cloud Spanner 的连接详细信息。

  • 启用 Spring 数据存储库(可选)。

159.1.1 Cloud Spanner 设置

您可以使用用于 Spring Data Spanner 的 Spring Boot Starter在 Spring 应用程序中自动配置 Google Cloud Spanner。它包含所有必要的设置,使您可以轻松地通过 Google Cloud 项目进行身份验证。提供以下配置选项:

NameDescriptionRequiredDefault value
spring.cloud.gcp.spanner.instance-id要使用的 Cloud Spanner 实例Yes
spring.cloud.gcp.spanner.database使用的 Cloud Spanner 数据库Yes
spring.cloud.gcp.spanner.project-id托管 Google Cloud Spanner API 的 GCP 项目 ID(如果与Spring Cloud GCP 核心模块中的 ID 不同)No
spring.cloud.gcp.spanner.credentials.location用于与 Google Cloud Spanner API 进行身份验证的 OAuth2 凭据(如果与Spring Cloud GCP 核心模块中的凭据不同)No
spring.cloud.gcp.spanner.credentials.encoded-key用于与 Google Cloud Spanner API 进行身份验证的 Base64 编码的 OAuth2 凭据(如果与Spring Cloud GCP 核心模块中的不同)No
spring.cloud.gcp.spanner.credentials.scopesOAuth2 scope用于 Spring Cloud GCP Cloud Spanner 凭据Nohttps://www.googleapis.com/auth/spanner.data
spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade如果为true,则SpannerSchemaUtils为具有交错的父子关系的表生成的架构语句将为“ ON DELETE CASCADE”。如果false,则表的模式将为“ ON DELETE NO ACTION”。Notrue
spring.cloud.gcp.spanner.numRpcChannels用于连接到 Cloud Spanner 的 gRPC 通道数No4 -由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.prefetchChunksCloud Spanner 为读取和查询预取的块数No4 -由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.minSessions会话池中维护的最小会话数No0 -由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.maxSessions会话池可以拥有的最大会话数No400 -由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.maxIdleSessions会话池将保持的最大空闲会话数No0 -由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.writeSessionsFraction要为写事务准备的会话比例No0 .2-由 Cloud SpannerClient 端库确定
spring.cloud.gcp.spanner.keepAliveIntervalMinutes保持空闲会话多长时间No30 -由 Cloud SpannerClient 端库确定

159.1.2 存储库设置

可以通过@Configuration主类上的@EnableSpannerRepositoriesComments 配置 Spring Data Repository。使用我们用于 Spring Data Cloud Spanner 的 Spring Boot Starter,会自动添加@EnableSpannerRepositories。不需要将其添加到任何其他类中,除非需要覆盖@EnableSpannerRepositories提供的更精细的粒度配置参数。

159.1.3 Autoconfiguration

我们的 Spring Boot 自动配置会在 Spring 应用程序上下文中创建以下可用的 bean:

  • SpannerTemplate的实例

  • SpannerDatabaseAdminTemplate的一个实例,用于从对象层次结构生成表架构以及创建和删除表和数据库

  • 启用存储库后,扩展了SpannerRepositoryCrudRepositoryPagingAndSortingRepository的所有用户定义存储库的实例

  • 来自 Google Cloud Java Client for Spanner 的DatabaseClient实例,以方便使用和访问较低级别的 API

159.2 对象 Map

Spring Data Cloud Spanner 允许您通过 Comments 将域 POJOMap 到 Cloud Spanner 表:

@Table(name = "traders")
public class Trader {

	@PrimaryKey
	@Column(name = "trader_id")
	String traderId;

	String firstName;

	String lastName;

	@NotMapped
	Double temporaryNumber;
}

Spring Data Cloud Spanner 将忽略带有@NotMappedComments 的任何属性。这些属性将不会写入或读取 Spanner。

159.2.1 Constructors

POJO 支持简单的构造函数。构造函数参数可以是持久性属性的子集。每个构造函数参数都必须具有与实体上的持久属性相同的名称和类型,构造函数应根据给定参数设置属性。不支持未直接设置为属性的参数。

@Table(name = "traders")
public class Trader {
	@PrimaryKey
	@Column(name = "trader_id")
	String traderId;

	String firstName;

	String lastName;

	@NotMapped
	Double temporaryNumber;

	public Trader(String traderId, String firstName) {
	    this.traderId = traderId;
	    this.firstName = firstName;
	}
}

159.2.2 Table

@TableComments 可以提供 Cloud Spanner 表的名称,该表存储带 Comments 的类的实例,每行一个。该 Comments 是可选的,如果未给出,则从类名推断出表名,且首字符不大写。

表名称的 SpEL 表达式

在某些情况下,您可能希望动态确定@Table表名。为此,您可以使用Spring 表达语言

For example:

@Table(name = "trades_#{tableNameSuffix}")
public class Trade {
	// ...
}

只有在 Spring 应用程序上下文中定义了tableNameSuffix值/ bean 时,才会解析表名。例如,如果tableNameSuffix的值为“ 123”,则表名将解析为trades_123

159.2.3 主键

对于一个简单的表,您可能只有一个由单列组成的主键。即使在这种情况下,也需要@PrimaryKeyComments。 @PrimaryKey标识与主键相对应的一个或多个 ID 属性。

Spanner 对多列复合主键具有一流的支持。您必须使用@PrimaryKeyComments 主键所包含的所有 POJO 字段,如下所示:

@Table(name = "trades")
public class Trade {
	@PrimaryKey(keyOrder = 2)
	@Column(name = "trade_id")
	private String tradeId;

	@PrimaryKey(keyOrder = 1)
	@Column(name = "trader_id")
	private String traderId;

	private String action;

	private Double price;

	private Double shares;

	private String symbol;
}

@PrimaryKeykeyOrder参数按 Sequences 标识与主键列相对应的属性,从 1 开始并连续增加。Sequences 很重要,必须反映出 Cloud Spanner 模式中定义的 Sequences。在我们的示例中,用于创建表的 DDL 及其主键如下:

CREATE TABLE trades (
    trader_id STRING(MAX),
    trade_id STRING(MAX),
    action STRING(15),
    symbol STRING(10),
    price FLOAT64,
    shares FLOAT64
) PRIMARY KEY (trader_id, trade_id)

扳手没有自动生成 ID。对于大多数用例,应谨慎使用 SequencesID,以避免在系统中创建数据热点。阅读扳手主键文档可以更好地了解主键和建议的做法。

159.2.4 Columns

POJO 上的所有可访问属性都将自动识别为 Cloud Spanner 列。列名由SpannerMappingContext bean 上默认定义的PropertyNameFieldNamingStrategy生成。 @ColumnComments 可以选择提供与属性和其他一些设置不同的列名:

  • name是列的可选名称

  • spannerTypeMaxLengthSTRINGBYTES列指定最大长度。仅在基于域类型生成 DDL 架构语句时使用此设置。

  • nullable指定是否将列创建为NOT NULL。仅在基于域类型生成 DDL 架构语句时使用此设置。

  • spannerType是您可以选择指定的 Cloud Spanner 列类型。如果未指定,则从 Java 属性类型推断兼容的列类型。

  • spannerCommitTimestamp是布尔值,指定此属性是否对应于自动填充的提交时间戳记列。写入 Cloud Spanner 时,将忽略此属性中设置的任何值。

159.2.5 嵌入式对象

如果将类型为B的对象嵌入为A的属性,则B的列将与A的列保存在同一 Cloud Spanner 表中。

如果B具有主键列,则这些列将包含在A的主键中。 B也可以具有嵌入式属性。嵌入允许在多个实体之间重复使用列,并且对于实现父子情况非常有用,因为 Cloud Spanner 要求子表包括其父项的关键列。

For example:

class X {
  @PrimaryKey
  String grandParentId;

  long age;
}

class A {
  @PrimaryKey
  @Embedded
  X grandParent;

  @PrimaryKey(keyOrder = 2)
  String parentId;

  String value;
}

@Table(name = "items")
class B {
  @PrimaryKey
  @Embedded
  A parent;

  @PrimaryKey(keyOrder = 2)
  String id;

  @Column(name = "child_value")
  String value;
}

B的实体可以存储在定义为的表中:

CREATE TABLE items (
    grandParentId STRING(MAX),
    parentId STRING(MAX),
    id STRING(MAX),
    value STRING(MAX),
    child_value STRING(MAX),
    age INT64
) PRIMARY KEY (grandParentId, parentId, id)

请注意,嵌入属性的列名称必须全部唯一。

159.2.6 Relationships

Spring Data Cloud Spanner 使用 Cloud Spanner 亲子交错表机制支持父子关系。 Cloud Spanner 交错表强制一对多关系,并在单个域父实体的实体上提供有效的查询和操作。这些关系最多可以达到 7 个层次。 Cloud Spanner 还提供了自动级联删除或强制在父级之前删除子实体。

虽然可以在 Cloud Spanner 和 Spring Data Cloud Spanner 中使用交错的父子表的构造实现一对一和多对多关系,但仅本地支持父子关系。 Cloud Spanner 不支持外键约束,尽管父子键约束在与交错表一起使用时会强制执行类似的要求。

例如,以下 Java 实体:

@Table(name = "Singers")
class Singer {
  @PrimaryKey
  long SingerId;

  String FirstName;

  String LastName;

  byte[] SingerInfo;

  @Interleaved
  List<Album> albums;
}

@Table(name = "Albums")
class Album {
  @PrimaryKey
  long SingerId;

  @PrimaryKey(keyOrder = 2)
  long AlbumId;

  String AlbumTitle;
}

这些类可以对应于一对现有的交错表。 @InterleavedComments 可以应用于Collection属性,并且内部类型被解析为子实体类型。创建它们所需的模式也可以使用SpannerSchemaUtils生成并使用SpannerDatabaseAdminTemplate执行:

@Autowired
SpannerSchemaUtils schemaUtils;

@Autowired
SpannerDatabaseAdminTemplate databaseAdmin;
...

// Get the create statmenets for all tables in the table structure rooted at Singer
List<String> createStrings = this.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.class);

// Create the tables and also create the database if necessary
this.databaseAdmin.executeDdlStrings(createStrings, true);

createStrings列表包含表架构语句,这些语句使用与提供的 Java 类型兼容的列名和类型,以及基于配置的自定义转换器而包含在其中的任何已解析子关系类型。

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

ON DELETE CASCADE子句表示如果删除歌手,Cloud Spanner 将删除该歌手的所有专辑。替代方法是ON DELETE NO ACTION,在此歌手必须先删除其所有专辑,然后才能删除。使用SpannerSchemaUtils生成架构字符串时,spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade布尔设置确定这些架构是针对true生成为ON DELETE CASCADE还是针对false生成为ON DELETE NO ACTION

Cloud Spanner 将这些关系限制为 7 个子层。一个表可能有多个子表。

在将对象更新或插入 Cloud Spanner 时,其所有引用的子对象也将分别更新或插入同一请求中。读取时,所有交错的子行也全部读取。

159.2.7 支持的类型

Spring Data Cloud Spanner 本机支持常规字段的以下类型,但也利用自定义转换器(在以下各节中详细介绍)和许多 sched 义的 Spring Data 自定义转换器来处理其他常见的 Java 类型。

本机支持的类型:

  • com.google.cloud.ByteArray

  • com.google.cloud.Date

  • com.google.cloud.Timestamp

  • java.lang.Boolean , boolean

  • java.lang.Double , double

  • java.lang.Long , long

  • java.lang.Integer , int

  • java.lang.String

  • double[]

  • long[]

  • boolean[]

  • java.util.Date

  • java.util.Instant

  • java.sql.Date

159.2.8 Lists

Spanner 支持ARRAY种列类型。 ARRAY列 Map 到 POJOS 中的List字段。

Example:

List<Double> curve;

列表内的类型可以是任何单个属性类型。

159.2.9 结构列表

Cloud Spanner 查询可以构造 STRUCT 值在结果中显示为列。 Cloud Spanner 要求 STRUCT 值出现在 ARRAY 中的根级别:SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users

Spring Data Cloud Spanner 将尝试将列 STRUCT 值读入一个属性,该属性是与列 STRUCT 值的模式兼容的实体类型的Iterable

对于前面的数组选择示例,可以将以下属性与构造的ARRAY<STRUCT>列 Map:List<TwoInts> pair;,其中定义了TwoInts类型:

class TwoInts {

  int val1;

  int val2;
}

159.2.10 自定义类型

定制转换器可用于扩展对用户定义类型的类型支持。

  • 转换器需要双向实现org.springframework.core.convert.converter.Converter接口。

  • 用户定义的类型需要 Map 到 Spanner 支持的基本类型之一:

  • com.google.cloud.ByteArray

  • com.google.cloud.Date

  • com.google.cloud.Timestamp

  • java.lang.Boolean , boolean

  • java.lang.Double , double

  • java.lang.Long , long

  • java.lang.String

  • double[]

  • long[]

  • boolean[]

  • enum

  • 这两个 Converter 的实例都需要传递给ConverterAwareMappingSpannerEntityProcessor,然后必须将其作为SpannerEntityProcessor@Bean使用。

For example:

我们希望在Trade POJO 上有一个类型为Person的字段:

@Table(name = "trades")
public class Trade {
  //...
  Person person;
  //...
}

其中 Person 是一个简单的类:

public class Person {

  public String firstName;
  public String lastName;

}

我们必须定义两个转换器:

public class PersonWriteConverter implements Converter<Person, String> {

    @Override
    public String convert(Person person) {
      return person.firstName + " " + person.lastName;
    }
  }

  public class PersonReadConverter implements Converter<String, Person> {

    @Override
    public Person convert(String s) {
      Person person = new Person();
      person.firstName = s.split(" ")[0];
      person.lastName = s.split(" ")[1];
      return person;
    }
  }

这将在我们的@Configuration文件中进行配置:

@Configuration
public class ConverterConfiguration {

	@Bean
	public SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) {
		return new ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext,
				Arrays.asList(new PersonWriteConverter()),
				Arrays.asList(new PersonReadConverter()));
	}
}

159.2.11 用于结构数组列的自定义转换器

如果提供了Converter<Struct, A>,则可以在您的实体类型中使用类型List<A>的属性。

159.3 扳手操作和模板

SpannerOperations及其实现SpannerTemplate提供了 Spring 开发人员熟悉的 Template 模式。它提供:

  • Resource management

  • 通过 Spring Data POJOMap 和转换功能一站式到 Spanner 操作

  • Exception conversion

使用我们的 Spanner 的 Spring Boot Starter 提供的autoconfigure,您的 Spring 应用程序上下文将包含一个完全配置的SpannerTemplate对象,您可以轻松地自动将其连接到应用程序中:

@SpringBootApplication
public class SpannerTemplateExample {

	@Autowired
	SpannerTemplate spannerTemplate;

	public void doSomething() {
		this.spannerTemplate.delete(Trade.class, KeySet.all());
		//...
		Trade t = new Trade();
		//...
		this.spannerTemplate.insert(t);
		//...
		List<Trade> tradesByAction = spannerTemplate.findAll(Trade.class);
		//...
	}
}

模板 API 提供了以下便捷方法:

  • Reads,并提供 SpannerReadOptions 和 SpannerQueryOptions

  • Stale read

    • 阅读二级索引

    • 读取限制和偏移

    • 阅读排序

  • Queries

  • DML 操作(删除,插入,更新,更新)

  • Partial reads

  • 您可以定义一组要读入实体的列

  • Partial writes

  • 仅保留您实体的一些属性

  • Read-only transactions

  • 锁定读写事务

159.3.1 SQL 查询

Cloud Spanner 具有运行只读查询的 SQL 支持。所有与查询相关的方法均以SpannerTemplate上的query开头。使用SpannerTemplate可以执行 Map 到 POJO 的 SQL 查询:

List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"));

159.3.2 Read

Spanner 公开了Read API,用于读取表或辅助索引中的单行或多行。

您可以使用SpannerTemplate执行读取,例如:

List<Trade> trades = this.spannerTemplate.readAll(Trade.class);

与查询相比,读取的主要好处是,使用KeySet类的功能可以更轻松地读取特定模式的键的多行。

159.3.3 高级阅读

Stale read

默认情况下,所有读取和查询都是“强读取”。 强读 是在当前时间戳下进行的读取,可以确保看到所有已提交的数据,直到该读取开始为止。另一方面,过去读取的是“陈旧读取”。 Cloud Spanner 允许您确定读取数据时数据的最新程度。使用SpannerTemplate,您可以通过将SpannerQueryOptionsSpannerReadOptions上的Timestamp设置为适当的读取或查询方法来指定Timestamp

Reads:

// a read with options:
SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setTimestamp(Timestamp.now());
List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

Queries:

// a query with options:
SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp(Timestamp.now());
List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

从二级索引读取

使用secondary index可通过模板 API 进行读取,也可通过 SQL for Queries 隐式使用。

下面显示了如何通过简单地通过在SpannerReadOptions上设置index来使用secondary index从表中读取行:

SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setIndex("TradesByTrader");
List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

读取偏移量和限制

限制和偏移量仅受查询支持。以下将仅获取查询的前两行:

SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setLimit(2).setOffset(3);
List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

注意,以上等效于执行SELECT * FROM trades LIMIT 2 OFFSET 3

Sorting

按键读取不支持排序。但是,对 Template API 的查询支持通过标准 SQL 以及 Spring Data Sort API 进行排序:

List<Trade> trades = this.spannerTemplate.queryAll(Trade.class, Sort.by("action"));

如果提供的排序字段名称是域类型属性的名称,则将在查询中使用与该属性对应的列名称。否则,假定给定的字段名称是 Cloud Spanner 表中列的名称。可以忽略大小写对 Cloud Spanner 类型 STRING 和 BYTES 的列进行排序:

Sort.by(Order.desc("action").ignoreCase())

Partial read

仅在使用查询时才可以进行部分读取。如果查询返回的行的列少于要 Map 到的实体的列,则 Spring Data 将仅 Map 返回的列。此设置也适用于嵌套结构及其相应的嵌套 POJO 属性。

List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT action, symbol FROM trades"),
    new SpannerQueryOptions().setAllowMissingResultSetColumns(true));

如果将设置设置为false,则查询结果中缺少列时将引发异常。

查询与读取的选项摘要

Feature查询支持阅读支持它
SQLyesno
Partial readyesno
Limitsyesno
Offsetsyesno
Secondary indexyesyes
使用索引范围读取noyes
Sortingyesno

159.3.4 写入/更新

SpannerOperations的 write 方法接受一个 POJO 并将其所有属性写入 Spanner。相应的 Spanner 表和实体元数据是从给定对象的实际类型获得的。

如果从 Spanner 检索了一个 POJO,并且更改了其主键属性值,然后对其进行了写入或更新,则该操作将针对具有新主键值的行进行。具有原始主键值的行将不受影响。

Insert

SpannerOperationsinsert方法接受 POJO 并将其所有属性写入 Spanner,这意味着如果表中已经存在带有 POJO 主键的行,则该操作将失败。

Trade t = new Trade();
this.spannerTemplate.insert(t);

Update

SpannerOperationsupdate方法接受 POJO 并将其所有属性写入 Spanner,这意味着如果表中不存在 POJO 的主键,则该操作将失败。

// t was retrieved from a previous operation
this.spannerTemplate.update(t);

Upsert

SpannerOperationsupsert方法接受 POJO,并使用 update-or-insert 将其所有属性写入 Spanner。

// t was retrieved from a previous operation or it's new
this.spannerTemplate.upsert(t);

Partial Update

默认情况下,SpannerOperations的更新方法对给定对象内的所有属性起作用,但也接受列名的String[]Optional<Set<String>>。如果列名称集的Optional为空,则所有列都将写入 Spanner。但是,如果 Optional 被空集占用,则不会写入任何列。

// t was retrieved from a previous operation or it's new
this.spannerTemplate.update(t, "symbol", "action");

159.3.5 DML

可以使用SpannerOperations.executeDmlStatement执行 DML 语句。插入,更新和删除可以影响任意数量的行和实体。

159.3.6 Transactions

SpannerOperations提供了在单个事务中运行java.util.Function对象的方法,同时使对SpannerOperations的读取和写入方法可用。

Read/Write Transaction

SpannerOperations通过performReadWriteTransaction方法提供读写事务:

@Autowired
SpannerOperations mySpannerOperations;

public String doWorkInsideTransaction() {
  return mySpannerOperations.performReadWriteTransaction(
    transActionSpannerOperations -> {
      // Work with transActionSpannerOperations here.
      // It is also a SpannerOperations object.

      return "transaction completed";
    }
  );
}

performReadWriteTransaction方法接受提供SpannerOperations对象实例的Function。函数的最终返回值和类型由用户确定。您可以像常规SpannerOperations一样使用此对象,但有一些 exception:

  • 它的读取功能无法执行陈旧的读取,因为所有读取和写入都在事务的单个时间点进行。

  • 它无法通过performReadWriteTransactionperformReadOnlyTransaction执行子 Transaction。

由于这些读写事务处于锁定状态,如果函数不执行任何写操作,建议您使用performReadOnlyTransaction

Read-only Transaction

performReadOnlyTransaction方法用于使用SpannerOperations执行只读事务:

@Autowired
SpannerOperations mySpannerOperations;

public String doWorkInsideTransaction() {
  return mySpannerOperations.performReadOnlyTransaction(
    transActionSpannerOperations -> {
      // Work with transActionSpannerOperations here.
      // It is also a SpannerOperations object.

      return "transaction completed";
    }
  );
}

performReadOnlyTransaction方法接受提供SpannerOperations对象实例的Function。此方法还接受ReadOptions对象,但使用的唯一属性是用于及时确定快照以在事务中执行读取的时间戳记。如果未在读取选项中设置时间戳,则将针对数据库的当前状态运行事务。函数的最终返回值和类型由用户确定。您可以像使用常规SpannerOperations一样使用此对象,但有一些 exception:

  • 它的读取功能无法执行陈旧的读取,因为所有读取都在事务的单个时间点发生。

  • 它无法通过performReadWriteTransactionperformReadOnlyTransaction执行子 Transaction

  • 它无法执行任何写操作。

由于只读事务是非锁定的,并且可以在过去的某个时间点执行,因此建议将这些事务用于不执行写操作的功能。

使用@Transactional 注解的声明式事务

此功能需要使用spring-cloud-gcp-starter-data-spanner时提供的SpannerTransactionManager bean。

SpannerTemplateSpannerRepository支持以@Transactional annotation作为事务的运行方法。如果用@TransactionalComments 的方法调用了也有 Comments 的另一个方法,则这两个方法将在同一事务中工作。 performReadOnlyTransactionperformReadWriteTransaction不能在@TransactionalComments 的方法中使用,因为 Cloud Spanner 不支持事务内的事务。

159.3.7 DML 语句

SpannerTemplate支持DMLStatements。 DML 语句可以通过performReadWriteTransaction或使用@TransactionalComments 在事务中执行。

当 DML 语句在事务之外执行时,它们将以partitioned-mode执行。

159.4 Repositories

Spring 数据仓库是功能强大的抽象,可以节省许多样板代码。

For example:

public interface TraderRepository extends SpannerRepository<Trader, String> {
}

Spring Data 生成指定接口的有效实现,可以方便地将其自动连接到应用程序中。

SpannerRepositoryTrader类型参数是指基础域类型。第二种类型参数String在这种情况下是指域类型的键的类型。

对于具有复合主键的 POJO,此 ID 类型参数可以是与所有主键属性兼容的Object[]的任何后代,Iterablecom.google.cloud.spanner.Key的任何后代。如果域 POJO 类型只有一个主键列,则可以使用主键属性类型或Key类型。

例如,在属于 Transaction 者的 Transaction 中,TradeRepository看起来像这样:

public interface TradeRepository extends SpannerRepository<Trade, String[]> {

}
public class MyApplication {

	@Autowired
	SpannerTemplate spannerTemplate;

	@Autowired
	StudentRepository studentRepository;

	public void demo() {

		this.tradeRepository.deleteAll();
		String traderId = "demo_trader";
		Trade t = new Trade();
		t.symbol = stock;
		t.action = action;
		t.traderId = traderId;
		t.price = 100.0;
		t.shares = 12345.6;
		this.spannerTemplate.insert(t);

		Iterable<Trade> allTrades = this.tradeRepository.findAll();

		int count = this.tradeRepository.countByAction("BUY");

	}
}

159.4.1 CRUD 存储库

CrudRepository方法按预期工作,但 Spanner 特有一件事:savesaveAll方法作为更新或插入工作。

159.4.2 分页和排序存储库

您也可以将PagingAndSortingRepository与 Spanner Spring Data 一起使用。此接口可用的排序和可分页的findAll方法在 Spanner 数据库的当前状态上运行。结果,当在页面之间移动时,请注意数据库的状态(和结果)可能会更改。

159.4.3 扳手 Repositories

SpannerRepository扩展了PagingAndSortingRepository,但是添加了 Spanner 提供的只读和读写事务功能。这些事务与SpannerOperations的工作非常相似,但是特定于存储库的域类型,并提供存储库功能而不是模板功能。

例如,这是一个读写事务:

@Autowired
SpannerRepository myRepo;

public String doWorkInsideTransaction() {
  return myRepo.performReadOnlyTransaction(
    transactionSpannerRepo -> {
      // Work with the single-transaction transactionSpannerRepo here.
      // This is a SpannerRepository object.

      return "transaction completed";
    }
  );
}

在为自己的域类型和查询方法创建自定义存储库时,您可以扩展SpannerRepository以访问特定于 Cloud Spanner 的功能以及PagingAndSortingRepositoryCrudRepository中的所有功能。

159.5 查询方法

SpannerRepository支持查询方法。在以下各节中将介绍这些方法,这些方法位于您的自定义存储库接口中,这些接口的实现是根据其名称和 Comments 生成的。查询方法可以读取,写入和删除 Cloud Spanner 中的实体。这些方法的参数可以是直接支持或通过自定义配置的转换器支持的任何 Cloud Spanner 数据类型。参数也可以是Struct或 POJO 类型。如果给定一个 POJO 作为参数,它将使用与创建写变异相同的类型转换逻辑转换为Struct。使用 Struct 参数的比较仅限于Cloud Spanner 提供了哪些功能

159.5.1 按约定查询方法

public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    List<Trade> findByAction(String action);

	int countByAction(String action);

	// Named methods are powerful, but can get unwieldy
	List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(
  			String action, String symbol, String traderId);
}

在上面的示例中,TradeRepository中的query methods是使用Spring Data Query 创建命名约定根据方法的名称生成的。

List<Trade> findByAction(String action)将转换为SELECT * FROM trades WHERE action = ?

函数List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId);将翻译为以下 SQL 查询的等效项:

SELECT DISTINCT * FROM trades
WHERE ACTION = ? AND LOWER(SYMBOL) = LOWER(?) AND TRADER_ID = ?
ORDER BY SYMBOL DESC
LIMIT 3

支持以下filter选项:

  • Equality

  • 大于或等于

  • Greater than

  • 小于或等于

  • Less than

  • Is null

  • 不为空

  • Is true

  • Is false

  • 像弦一样

  • 不像字符串

  • 包含一个字符串

  • 不包含字符串

请注意,短语SymbolIgnoreCase被翻译为LOWER(SYMBOL) = LOWER(?),表示不区分大小写。 IgnoreCase短语只能附加到与 STRING 或 BYTES 类型的列相对应的字段中。不支持在方法名称末尾附加的 Spring Data“ AllIgnoreCase”短语。

LikeNotLike命名约定:

List<Trade> findBySymbolLike(String symbolFragment);

参数symbolFragment可以包含wildcard characters以进行字符串匹配,例如_%

ContainsNotContains命名约定:

List<Trade> findBySymbolContains(String symbolFragment);

参数symbolFragment是被检查是否存在的regular expression

还支持删除查询。例如,诸如deleteByActionremoveByAction之类的查询方法会删除findByAction找到的实体。删除操作发生在单个事务中。

删除查询可以具有以下返回类型:整数类型,它是删除的实体数删除的实体的集合* void

159.5.2 自定义 SQL/DML 查询方法

上面List<Trade> fetchByActionNamedQuery(String action)的示例与Spring Data Query 创建命名约定不匹配,因此我们必须将参数化的 Spanner SQL 查询 Map 到它。

可以通过以下两种方式之一将方法的 SQL 查询 Map 到存储库方法:

  • namedQueries属性文件

  • 使用@Query注解

SQL 的标记名称与方法参数的@ParamComments 名称相对应。

自定义 SQL 查询方法可以接受单个SortPageable参数,该参数将应用于 SQL 中的任何排序或分页:

@Query("SELECT * FROM trades ORDER BY action DESC")
	List<Trade> sortedTrades(Pageable pageable);

	@Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1")
 	Trade sortedTopTrade(Pageable pageable);

可以使用:

List<Trade> customSortedTrades = tradeRepository.sortedTrades(PageRequest
  				.of(2, 2, org.springframework.data.domain.Sort.by(Order.asc("id"))));

结果将按“ id”以升序排序。

您的查询方法还可以返回非实体类型:

@Query("SELECT COUNT(1) FROM trades WHERE action = @action")
  	int countByActionQuery(String action);

  	@Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)")
  	boolean existsByActionQuery(String action);

  	@Query("SELECT action FROM trades WHERE action = @action LIMIT 1")
  	String getFirstString(@Param("action") String action);

  	@Query("SELECT action FROM trades WHERE action = @action")
  	List<String> getFirstStringList(@Param("action") String action);

DML 语句也可以通过查询方法执行,但是唯一可能的返回值是long,代表受影响的行数。必须在@Query上设置dmlStatement布尔设置,以指示查询方法作为 DML 语句执行。

@Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true)
  	long deleteByActionQuery(String action);

具有命名查询属性的查询方法

默认情况下,@EnableSpannerRepositories上的namedQueriesLocation属性指向META-INF/spanner-named-queries.properties文件。您可以通过提供 SQL 作为“ interface.method”属性的值来在属性文件中指定方法的查询:

Trade.fetchByActionNamedQuery=SELECT * FROM trades WHERE trades.action = @tag0
public interface TradeRepository extends SpannerRepository<Trade, String[]> {
	// This method uses the query from the properties file instead of one generated based on name.
	List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
}

带 Comments 的查询方法

使用@Query注解:

public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    @Query("SELECT * FROM trades WHERE trades.action = @tag0")
    List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
}

表名可以直接使用。例如,以上示例中的“Transaction”。另外,也可以从域类的@TableComments 中解析表名。在这种情况下,查询应引用表名,这些表名的全限定类名在:个字符之间::fully.qualified.ClassName:。完整的示例如下所示:

@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0")
List<Trade> fetchByActionNamedQuery(String action);

这允许在自定义查询中使用通过 SpEL 评估的表名。

SpEL 还可以用于提供 SQL 参数:

@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0
  AND price > #{#priceRadius * -1} AND price < #{#priceRadius * 2}")
List<Trade> fetchByActionNamedQuery(String action, Double priceRadius);

159.5.3 Projections

Spring Data Spanner 支持projections。您可以根据域类型定义投影接口,并添加在存储库中返回它们的查询方法:

public interface TradeProjection {

	String getAction();

	@Value("#{target.symbol + ' ' + target.action}")
	String getSymbolAndAction();
}

public interface TradeRepository extends SpannerRepository<Trade, Key> {

	List<Trade> findByTraderId(String traderId);

	List<TradeProjection> findByAction(String action);

	@Query("SELECT action, symbol FROM trades WHERE action = @action")
	List<TradeProjection> findByQuery(String action);
}

可以通过基于名称约定的查询方法以及自定义 SQL 查询来提供投影。如果使用自定义 SQL 查询,则可以进一步限制从 Spanner 检索的列,使其仅限于投影所需的列,以提高性能。

使用 SpEL 定义的投影类型的属性为基础域对象使用固定名称target。结果,访问基础属性的格式为target.<property-name>

159.5.4 RESTRepositories

通过 Spring Boot 运行时,只需将以下依赖项添加到 pom 文件中,即可将存储库作为 REST 服务公开:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

如果您希望配置参数(例如路径),则可以使用@RepositoryRestResourceComments:

@RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
public interface TradeRepository extends SpannerRepository<Trade, String[]> {
}

例如,您可以使用curl http://<server>:<port>/trades检索存储库中的所有Trade对象,或者通过curl http://<server>:<port>/trades/<trader_id>,<trade_id>进行任何特定 Transaction。

主键组件idtrader_id之间的分隔符在这种情况下默认情况下是逗号,但可以通过扩展SpannerKeyIdConverter类将其配置为在键值中找不到的任何字符串:

@Component
class MySpecialIdConverter extends SpannerKeyIdConverter {

    @Override
    protected String getUrlIdSeparator() {
        return ":";
    }
}

您也可以使用curl -XPOST -H"Content-Type: application/json" -[email protected] http://<server>:<port>/trades/进行 Transaction,其中文件test.json包含Trade对象的 JSON 表示形式。

159.6 数据库和架构 Management 员

Spanner 实例中的数据库和表可以从SpannerPersistentEntity个对象中自动创建:

@Autowired
private SpannerSchemaUtils spannerSchemaUtils;

@Autowired
private SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate;

public void createTable(SpannerPersistentEntity entity) {
	if(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){

	  // The boolean parameter indicates that the database will be created if it does not exist.
	  spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList(
            spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true);
	}
}

可以为具有交错关系和组合键的整个对象层次结构生成模式。

159.7 Sample

sample application可用。