Files
spring-cloud-static/Greenwich.SR1/multi/multi__spring_data_cloud_spanner.html
2019-03-06 10:23:45 -05:00

465 lines
99 KiB
HTML

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>158.&nbsp;Spring Data Cloud Spanner</title><link rel="stylesheet" type="text/css" href="css/manual-multipage.css"><meta name="generator" content="DocBook XSL Stylesheets V1.79.1"><link rel="home" href="multi_spring-cloud.html" title="Spring Cloud"><link rel="up" href="multi_spring-cloud-gcp-reference.html" title="Part&nbsp;XVIII.&nbsp;Spring Cloud GCP"><link rel="prev" href="multi__spring_cloud_config_2.html" title="157.&nbsp;Spring Cloud Config"><link rel="next" href="multi__spring_data_cloud_datastore.html" title="159.&nbsp;Spring Data Cloud Datastore"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">158.&nbsp;Spring Data Cloud Spanner</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="multi__spring_cloud_config_2.html">Prev</a>&nbsp;</td><th width="60%" align="center">Part&nbsp;XVIII.&nbsp;Spring Cloud GCP</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="multi__spring_data_cloud_datastore.html">Next</a></td></tr></table><hr></div><div class="chapter"><div class="titlepage"><div><div><h2 class="title"><a name="_spring_data_cloud_spanner" href="#_spring_data_cloud_spanner"></a>158.&nbsp;Spring Data Cloud Spanner</h2></div></div></div><p><a class="link" href="http://projects.spring.io/spring-data/" target="_top">Spring Data</a> is an abstraction for storing and retrieving POJOs in numerous storage technologies.
Spring Cloud GCP adds Spring Data support for <a class="link" href="http://cloud.google.com/spanner/" target="_top">Google Cloud Spanner</a>.</p><p>Maven coordinates for this module only, using Spring Cloud GCP BOM:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;dependency&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;groupId&gt;</span>org.springframework.cloud<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/groupId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;artifactId&gt;</span>spring-cloud-gcp-data-spanner<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/artifactId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/dependency&gt;</span></pre><p>Gradle coordinates:</p><pre class="screen">dependencies {
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner'
}</pre><p>We provide a <a class="link" href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner" target="_top">Spring Boot Starter for Spring Data Spanner</a>, with which you can leverage our recommended auto-configuration setup.
To use the starter, see the coordinates see below.</p><p>Maven:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;dependency&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;groupId&gt;</span>org.springframework.cloud<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/groupId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;artifactId&gt;</span>spring-cloud-gcp-starter-data-spanner<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/artifactId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/dependency&gt;</span></pre><p>Gradle:</p><pre class="screen">dependencies {
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner'
}</pre><p>This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Spanner libraries as well.</p><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_configuration_6" href="#_configuration_6"></a>158.1&nbsp;Configuration</h2></div></div></div><p>To setup Spring Data Cloud Spanner, you have to configure the following:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">Setup the connection details to Google Cloud Spanner.</li><li class="listitem">Enable Spring Data Repositories (optional).</li></ul></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_cloud_spanner_settings" href="#_cloud_spanner_settings"></a>158.1.1&nbsp;Cloud Spanner settings</h3></div></div></div><p>You can the use <a class="link" href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner" target="_top">Spring Boot Starter for Spring Data Spanner</a> to autoconfigure Google Cloud Spanner in your Spring application.
It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project.
The following configuration options are available:</p><div class="informaltable"><table class="informaltable" style="border-collapse: collapse;border-top: 1px solid ; border-bottom: 1px solid ; "><colgroup><col class="col_1"><col class="col_2"><col class="col_3"><col class="col_4"></colgroup><tbody><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Name</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Description</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Required</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>Default value</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.instance-id</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Cloud Spanner instance to use</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top">&nbsp;</td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.database</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Cloud Spanner database to use</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top">&nbsp;</td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.project-id</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>GCP project ID where the Google Cloud Spanner API is hosted, if different from the one in the <a class="link" href="multi_spring-cloud-gcp-core.html" title="149.&nbsp;Spring Cloud GCP Core">Spring Cloud GCP Core Module</a></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top">&nbsp;</td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.credentials.location</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>OAuth2 credentials for authenticating with the
Google Cloud Spanner API, if different from the ones in the
<a class="link" href="multi_spring-cloud-gcp-core.html" title="149.&nbsp;Spring Cloud GCP Core">Spring Cloud GCP Core Module</a></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top">&nbsp;</td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.credentials.encoded-key</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Base64-encoded OAuth2 credentials for authenticating with the
Google Cloud Spanner API, if different from the ones in the
<a class="link" href="multi_spring-cloud-gcp-core.html" title="149.&nbsp;Spring Cloud GCP Core">Spring Cloud GCP Core Module</a></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top">&nbsp;</td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.credentials.scopes</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><a class="link" href="https://developers.google.com/identity/protocols/googlescopes" target="_top">OAuth2 scope</a> for Spring Cloud GCP
Cloud Spanner credentials</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p><a class="link" href="https://www.googleapis.com/auth/spanner.data" target="_top">https://www.googleapis.com/auth/spanner.data</a></p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>If <code class="literal">true</code>, then schema statements generated by <code class="literal">SpannerSchemaUtils</code> for tables with interleaved parent-child relationships will be "ON DELETE CASCADE".
The schema for the tables will be "ON DELETE NO ACTION" if <code class="literal">false</code>.</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">true</code></p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.numRpcChannels</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Number of gRPC channels used to connect to Cloud Spanner</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>4 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.prefetchChunks</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Number of chunks prefetched by Cloud Spanner for read and query</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>4 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.minSessions</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Minimum number of sessions maintained in the session pool</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>0 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.maxSessions</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Maximum number of sessions session pool can have</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>400 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.maxIdleSessions</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Maximum number of idle sessions session pool will maintain</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>0 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.writeSessionsFraction</code></p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Fraction of sessions to be kept prepared for write transactions</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>0.2 - Determined by Cloud Spanner client library</p></td></tr><tr><td style="border-right: 1px solid ; " align="left" valign="top"><p><code class="literal">spring.cloud.gcp.spanner.keepAliveIntervalMinutes</code></p></td><td style="border-right: 1px solid ; " align="left" valign="top"><p>How long to keep idle sessions alive</p></td><td style="border-right: 1px solid ; " align="left" valign="top"><p>No</p></td><td style="" align="left" valign="top"><p>30 - Determined by Cloud Spanner client library</p></td></tr></tbody></table></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_repository_settings" href="#_repository_settings"></a>158.1.2&nbsp;Repository settings</h3></div></div></div><p>Spring Data Repositories can be configured via the <code class="literal">@EnableSpannerRepositories</code> annotation on your main <code class="literal">@Configuration</code> class.
With our Spring Boot Starter for Spring Data Cloud Spanner, <code class="literal">@EnableSpannerRepositories</code> is automatically added.
It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by <a class="link" href="https://github.com/spring-cloud/spring-cloud-gcp/blob/master/spring-cloud-gcp-data-spanner/src/main/java/org/springframework/cloud/gcp/data/spanner/repository/config/EnableSpannerRepositories.java" target="_top"><code class="literal">@EnableSpannerRepositories</code></a>.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_autoconfiguration" href="#_autoconfiguration"></a>158.1.3&nbsp;Autoconfiguration</h3></div></div></div><p>Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">an instance of <code class="literal">SpannerTemplate</code></li><li class="listitem">an instance of <code class="literal">SpannerDatabaseAdminTemplate</code> for generating table schemas from object hierarchies and creating and deleting tables and databases</li><li class="listitem">an instance of all user-defined repositories extending <code class="literal">SpannerRepository</code>, <code class="literal">CrudRepository</code>, <code class="literal">PagingAndSortingRepository</code>, when repositories are enabled</li><li class="listitem">an instance of <code class="literal">DatabaseClient</code> from the Google Cloud Java Client for Spanner, for convenience and lower level API access</li></ul></div></div></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_object_mapping" href="#_object_mapping"></a>158.2&nbsp;Object Mapping</h2></div></div></div><p>Spring Data Cloud Spanner allows you to map domain POJOs to Cloud Spanner tables via annotations:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "traders")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Trader {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<em><span class="hl-annotation" style="color: gray">@Column(name = "trader_id")</span></em>
String traderId;
String firstName;
String lastName;
<em><span class="hl-annotation" style="color: gray">@NotMapped</span></em>
Double temporaryNumber;
}</pre><p>Spring Data Cloud Spanner will ignore any property annotated with <code class="literal">@NotMapped</code>.
These properties will not be written to or read from Spanner.</p><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_constructors" href="#_constructors"></a>158.2.1&nbsp;Constructors</h3></div></div></div><p>Simple constructors are supported on POJOs.
The constructor arguments can be a subset of the persistent properties.
Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument.
Arguments that are not directly set to properties are not supported.</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "traders")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Trader {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<em><span class="hl-annotation" style="color: gray">@Column(name = "trader_id")</span></em>
String traderId;
String firstName;
String lastName;
<em><span class="hl-annotation" style="color: gray">@NotMapped</span></em>
Double temporaryNumber;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> Trader(String traderId, String firstName) {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.traderId = traderId;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.firstName = firstName;
}
}</pre></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_table" href="#_table"></a>158.2.2&nbsp;Table</h3></div></div></div><p>The <code class="literal">@Table</code> annotation can provide the name of the Cloud Spanner table that stores instances of the annotated class, one per row.
This annotation is optional, and if not given, the name of the table is inferred from the class name with the first character uncapitalized.</p><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_spel_expressions_for_table_names" href="#_spel_expressions_for_table_names"></a>SpEL expressions for table names</h4></div></div></div><p>In some cases, you might want the <code class="literal">@Table</code> table name to be determined dynamically.
To do that, you can use <a class="link" href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions" target="_top">Spring Expression Language</a>.</p><p>For example:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "trades_#{tableNameSuffix}")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Trade {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// ...</span>
}</pre><p>The table name will be resolved only if the <code class="literal">tableNameSuffix</code> value/bean in the Spring application context is defined.
For example, if <code class="literal">tableNameSuffix</code> has the value "123", the table name will resolve to <code class="literal">trades_123</code>.</p></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_primary_keys" href="#_primary_keys"></a>158.2.3&nbsp;Primary Keys</h3></div></div></div><p>For a simple table, you may only have a primary key consisting of a single column.
Even in that case, the <code class="literal">@PrimaryKey</code> annotation is required.
<code class="literal">@PrimaryKey</code> identifies the one or more ID properties corresponding to the primary key.</p><p>Spanner has first class support for composite primary keys of multiple columns.
You have to annotate all of your POJO&#8217;s fields that the primary key consists of with <code class="literal">@PrimaryKey</code> as below:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "trades")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Trade {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey(keyOrder = 2)</span></em>
<em><span class="hl-annotation" style="color: gray">@Column(name = "trade_id")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> String tradeId;
<em><span class="hl-annotation" style="color: gray">@PrimaryKey(keyOrder = 1)</span></em>
<em><span class="hl-annotation" style="color: gray">@Column(name = "trader_id")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> String traderId;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> String action;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> Double price;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> Double shares;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> String symbol;
}</pre><p>The <code class="literal">keyOrder</code> parameter of <code class="literal">@PrimaryKey</code> identifies the properties corresponding to the primary key columns in order, starting with 1 and increasing consecutively.
Order is important and must reflect the order defined in the Cloud Spanner schema.
In our example the DDL to create the table and its primary key is as follows:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">CREATE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">TABLE</span> trades (
trader_id STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
trade_id STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">action</span> STRING(<span class="hl-number">15</span>),
symbol STRING(<span class="hl-number">10</span>),
price FLOAT64,
shares FLOAT64
) <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">PRIMARY</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">KEY</span> (trader_id, trade_id)</pre><p>Spanner does not have automatic ID generation.
For most use-cases, sequential IDs should be used with caution to avoid creating data hotspots in the system.
Read <a class="link" href="https://cloud.google.com/spanner/docs/schema-and-data-model#primary_keys" target="_top">Spanner Primary Keys documentation</a> for a better understanding of primary keys and recommended practices.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_columns" href="#_columns"></a>158.2.4&nbsp;Columns</h3></div></div></div><p>All accessible properties on POJOs are automatically recognized as a Cloud Spanner column.
Column naming is generated by the <code class="literal">PropertyNameFieldNamingStrategy</code> by default defined on the <code class="literal">SpannerMappingContext</code> bean.
The <code class="literal">@Column</code> annotation optionally provides a different column name than that of the property and some other settings:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><code class="literal">name</code> is the optional name of the column</li><li class="listitem"><code class="literal">spannerTypeMaxLength</code> specifies for <code class="literal">STRING</code> and <code class="literal">BYTES</code> columns the maximum length.
This setting is only used when generating DDL schema statements based on domain types.</li><li class="listitem"><code class="literal">nullable</code> specifies if the column is created as <code class="literal">NOT NULL</code>.
This setting is only used when generating DDL schema statements based on domain types.</li><li class="listitem"><code class="literal">spannerType</code> is the Cloud Spanner column type you can optionally specify.
If this is not specified then a compatible column type is inferred from the Java property type.</li><li class="listitem"><code class="literal">spannerCommitTimestamp</code> is a boolean specifying if this property corresponds to an auto-populated commit timestamp column.
Any value set in this property will be ignored when writing to Cloud Spanner.</li></ul></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_embedded_objects" href="#_embedded_objects"></a>158.2.5&nbsp;Embedded Objects</h3></div></div></div><p>If an object of type <code class="literal">B</code> is embedded as a property of <code class="literal">A</code>, then the columns of <code class="literal">B</code> will be saved in the same Cloud Spanner table as those of <code class="literal">A</code>.</p><p>If <code class="literal">B</code> has primary key columns, those columns will be included in the primary key of <code class="literal">A</code>. <code class="literal">B</code> can also have embedded properties.
Embedding allows reuse of columns between multiple entities, and can be useful for implementing parent-child situations, because Cloud Spanner requires child tables to include the key columns of their parents.</p><p>For example:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> X {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
String grandParentId;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">long</span> age;
}
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> A {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<em><span class="hl-annotation" style="color: gray">@Embedded</span></em>
X grandParent;
<em><span class="hl-annotation" style="color: gray">@PrimaryKey(keyOrder = 2)</span></em>
String parentId;
String value;
}
<em><span class="hl-annotation" style="color: gray">@Table(name = "items")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> B {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<em><span class="hl-annotation" style="color: gray">@Embedded</span></em>
A parent;
<em><span class="hl-annotation" style="color: gray">@PrimaryKey(keyOrder = 2)</span></em>
String id;
<em><span class="hl-annotation" style="color: gray">@Column(name = "child_value")</span></em>
String value;
}</pre><p>Entities of <code class="literal">B</code> can be stored in a table defined as:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">CREATE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">TABLE</span> items (
grandParentId STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
parentId STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
id STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">value</span> STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
child_value STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
age INT64
) <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">PRIMARY</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">KEY</span> (grandParentId, parentId, id)</pre><p>Note that embedded properties' column names must all be unique.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_relationships" href="#_relationships"></a>158.2.6&nbsp;Relationships</h3></div></div></div><p>Spring Data Cloud Spanner supports parent-child relationships using the Cloud Spanner <a class="link" href="https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables" target="_top">parent-child interleaved table mechanism</a>.
Cloud Spanner interleaved tables enforce the one-to-many relationship and provide efficient queries and operations on entities of a single domain parent entity.
These relationships can be up to 7 levels deep.
Cloud Spanner also provides automatic cascading delete or enforces the deletion of child entities before parents.</p><p>While one-to-one and many-to-many relationships can be implemented in Cloud Spanner and Spring Data Cloud Spanner using constructs of interleaved parent-child tables, only the parent-child relationship is natively supported.
Cloud Spanner does not support the foreign key constraint, though the parent-child key constraint enforces a similar requirement when used with interleaved tables.</p><p>For example, the following Java entities:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "Singers")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Singer {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">long</span> SingerId;
String FirstName;
String LastName;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">byte</span>[] SingerInfo;
<em><span class="hl-annotation" style="color: gray">@Interleaved</span></em>
List&lt;Album&gt; albums;
}
<em><span class="hl-annotation" style="color: gray">@Table(name = "Albums")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Album {
<em><span class="hl-annotation" style="color: gray">@PrimaryKey</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">long</span> SingerId;
<em><span class="hl-annotation" style="color: gray">@PrimaryKey(keyOrder = 2)</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">long</span> AlbumId;
String AlbumTitle;
}</pre><p>These classes can correspond to an existing pair of interleaved tables.
The <code class="literal">@Interleaved</code> annotation may be applied to <code class="literal">Collection</code> properties and the inner type is resolved as the child entity type.
The schema needed to create them can also be generated using the <code class="literal">SpannerSchemaUtils</code> and executed using the <code class="literal">SpannerDatabaseAdminTemplate</code>:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerSchemaUtils schemaUtils;
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerDatabaseAdminTemplate databaseAdmin;
...
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Get the create statmenets for all tables in the table structure rooted at Singer</span>
List&lt;String&gt; createStrings = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Create the tables and also create the database if necessary</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.databaseAdmin.executeDdlStrings(createStrings, true);</pre><p>The <code class="literal">createStrings</code> list contains table schema statements using column names and types compatible with the provided Java type and any resolved child relationship types contained within based on the configured custom converters.</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">CREATE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">TABLE</span> Singers (
SingerId INT64 <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NOT</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NULL</span>,
FirstName STRING(<span class="hl-number">1024</span>),
LastName STRING(<span class="hl-number">1024</span>),
SingerInfo BYTES(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
) <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">PRIMARY</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">KEY</span> (SingerId);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">CREATE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">TABLE</span> Albums (
SingerId INT64 <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NOT</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NULL</span>,
AlbumId INT64 <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NOT</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">NULL</span>,
AlbumTitle STRING(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">MAX</span>),
) <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">PRIMARY</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">KEY</span> (SingerId, AlbumId),
INTERLEAVE <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">IN</span> PARENT Singers <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">ON</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">DELETE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">CASCADE</span>;</pre><p>The <code class="literal">ON DELETE CASCADE</code> clause indicates that Cloud Spanner will delete all Albums of a singer if the Singer is deleted.
The alternative is <code class="literal">ON DELETE NO ACTION</code>, where a Singer cannot be deleted until all of its Albums have already been deleted.
When using <code class="literal">SpannerSchemaUtils</code> to generate the schema strings, the <code class="literal">spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade</code> boolean setting determines if these schema are generated as <code class="literal">ON DELETE CASCADE</code> for <code class="literal">true</code> and <code class="literal">ON DELETE NO ACTION</code> for <code class="literal">false</code>.</p><p>Cloud Spanner restricts these relationships to 7 child layers.
A table may have multiple child tables.</p><p>On updating or inserting an object to Cloud Spanner, all of its referenced children objects are also updated or inserted in the same request, respectively.
On read, all of the interleaved child rows are also all read.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_supported_types" href="#_supported_types"></a>158.2.7&nbsp;Supported Types</h3></div></div></div><p>Spring Data Cloud Spanner natively supports the following types for regular fields but also utilizes custom converters (detailed in following sections) and dozens of pre-defined Spring Data custom converters to handle other common Java types.</p><p>Natively supported types:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><code class="literal">com.google.cloud.ByteArray</code></li><li class="listitem"><code class="literal">com.google.cloud.Date</code></li><li class="listitem"><code class="literal">com.google.cloud.Timestamp</code></li><li class="listitem"><code class="literal">java.lang.Boolean</code>, <code class="literal">boolean</code></li><li class="listitem"><code class="literal">java.lang.Double</code>, <code class="literal">double</code></li><li class="listitem"><code class="literal">java.lang.Long</code>, <code class="literal">long</code></li><li class="listitem"><code class="literal">java.lang.Integer</code>, <code class="literal">int</code></li><li class="listitem"><code class="literal">java.lang.String</code></li><li class="listitem"><code class="literal">double[]</code></li><li class="listitem"><code class="literal">long[]</code></li><li class="listitem"><code class="literal">boolean[]</code></li><li class="listitem"><code class="literal">java.util.Date</code></li><li class="listitem"><code class="literal">java.util.Instant</code></li><li class="listitem"><code class="literal">java.sql.Date</code></li></ul></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_lists" href="#_lists"></a>158.2.8&nbsp;Lists</h3></div></div></div><p>Spanner supports <code class="literal">ARRAY</code> types for columns.
<code class="literal">ARRAY</code> columns are mapped to <code class="literal">List</code> fields in POJOS.</p><p>Example:</p><pre class="programlisting">List&lt;Double&gt; curve;</pre><p>The types inside the lists can be any singular property type.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_lists_of_structs" href="#_lists_of_structs"></a>158.2.9&nbsp;Lists of Structs</h3></div></div></div><p>Cloud Spanner queries can <a class="link" href="https://cloud.google.com/spanner/docs/query-syntax#using-structs-with-select" target="_top">construct STRUCT values</a> that appear as columns in the result.
Cloud Spanner requires STRUCT values appear in ARRAYs at the root level: <code class="literal">SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users</code>.</p><p>Spring Data Cloud Spanner will attempt to read the column STRUCT values into a property that is an <code class="literal">Iterable</code> of an entity type compatible with the schema of the column STRUCT value.</p><p>For the previous array-select example, the following property can be mapped with the constructed <code class="literal">ARRAY&lt;STRUCT&gt;</code> column: <code class="literal">List&lt;TwoInts&gt; pair;</code> where the <code class="literal">TwoInts</code> type is defined:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> TwoInts {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">int</span> val1;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">int</span> val2;
}</pre></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_custom_types" href="#_custom_types"></a>158.2.10&nbsp;Custom types</h3></div></div></div><p>Custom converters can be used to extend the type support for user defined types.</p><div class="orderedlist"><ol class="orderedlist" type="1"><li class="listitem">Converters need to implement the <code class="literal">org.springframework.core.convert.converter.Converter</code> interface in both directions.</li><li class="listitem"><p class="simpara">The user defined type needs to be mapped to one of the basic types supported by Spanner:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><code class="literal">com.google.cloud.ByteArray</code></li><li class="listitem"><code class="literal">com.google.cloud.Date</code></li><li class="listitem"><code class="literal">com.google.cloud.Timestamp</code></li><li class="listitem"><code class="literal">java.lang.Boolean</code>, <code class="literal">boolean</code></li><li class="listitem"><code class="literal">java.lang.Double</code>, <code class="literal">double</code></li><li class="listitem"><code class="literal">java.lang.Long</code>, <code class="literal">long</code></li><li class="listitem"><code class="literal">java.lang.String</code></li><li class="listitem"><code class="literal">double[]</code></li><li class="listitem"><code class="literal">long[]</code></li><li class="listitem"><code class="literal">boolean[]</code></li><li class="listitem"><code class="literal">enum</code> types</li></ul></div></li><li class="listitem">An instance of both Converters needs to be passed to a <code class="literal">ConverterAwareMappingSpannerEntityProcessor</code>, which then has to be made available as a <code class="literal">@Bean</code> for <code class="literal">SpannerEntityProcessor</code>.</li></ol></div><p>For example:</p><p>We would like to have a field of type <code class="literal">Person</code> on our <code class="literal">Trade</code> POJO:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Table(name = "trades")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Trade {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
Person person;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
}</pre><p>Where Person is a simple class:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> Person {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String firstName;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String lastName;
}</pre><p>We have to define the two converters:</p><pre class="programlisting"> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> PersonWriteConverter <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">implements</span> Converter&lt;Person, String&gt; {
<em><span class="hl-annotation" style="color: gray">@Override</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String convert(Person person) {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> person.firstName + <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">" "</span> + person.lastName;
}
}
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> PersonReadConverter <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">implements</span> Converter&lt;String, Person&gt; {
<em><span class="hl-annotation" style="color: gray">@Override</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> Person convert(String s) {
Person person = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> Person();
person.firstName = s.split(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">" "</span>)[<span class="hl-number">0</span>];
person.lastName = s.split(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">" "</span>)[<span class="hl-number">1</span>];
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> person;
}
}</pre><p>That will be configured in our <code class="literal">@Configuration</code> file:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Configuration</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> ConverterConfiguration {
<em><span class="hl-annotation" style="color: gray">@Bean</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext,
Arrays.asList(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> PersonWriteConverter()),
Arrays.asList(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> PersonReadConverter()));
}
}</pre></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_custom_converter_for_struct_array_columns" href="#_custom_converter_for_struct_array_columns"></a>158.2.11&nbsp;Custom Converter for Struct Array Columns</h3></div></div></div><p>If a <code class="literal">Converter&lt;Struct, A&gt;</code> is provided, then properties of type <code class="literal">List&lt;A&gt;</code> can be used in your entity types.</p></div></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_spanner_operations_template" href="#_spanner_operations_template"></a>158.3&nbsp;Spanner Operations &amp; Template</h2></div></div></div><p><code class="literal">SpannerOperations</code> and its implementation, <code class="literal">SpannerTemplate</code>, provides the Template pattern familiar to Spring developers.
It provides:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">Resource management</li><li class="listitem">One-stop-shop to Spanner operations with the Spring Data POJO mapping and conversion features</li><li class="listitem">Exception conversion</li></ul></div><p>Using the <code class="literal">autoconfigure</code> provided by our Spring Boot Starter for Spanner, your Spring application context will contain a fully configured <code class="literal">SpannerTemplate</code> object that you can easily autowire in your application:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@SpringBootApplication</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> SpannerTemplateExample {
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerTemplate spannerTemplate;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">void</span> doSomething() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.delete(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, KeySet.all());
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
Trade t = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> Trade();
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.insert(t);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
List&lt;Trade&gt; tradesByAction = spannerTemplate.findAll(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">//...</span>
}
}</pre><p>The Template API provides convenience methods for:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><p class="simpara"><a class="link" href="https://cloud.google.com/spanner/docs/reads" target="_top">Reads</a>, and by providing SpannerReadOptions and
SpannerQueryOptions</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: circle; "><li class="listitem">Stale read</li><li class="listitem">Read with secondary indices</li><li class="listitem">Read with limits and offsets</li><li class="listitem">Read with sorting</li></ul></div></li><li class="listitem"><a class="link" href="https://cloud.google.com/spanner/docs/reads#execute_a_query" target="_top">Queries</a></li><li class="listitem">DML operations (delete, insert, update, upsert)</li><li class="listitem"><p class="simpara">Partial reads</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: circle; "><li class="listitem">You can define a set of columns to be read into your entity</li></ul></div></li><li class="listitem"><p class="simpara">Partial writes</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: circle; "><li class="listitem">Persist only a few properties from your entity</li></ul></div></li><li class="listitem">Read-only transactions</li><li class="listitem">Locking read-write transactions</li></ul></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_sql_query" href="#_sql_query"></a>158.3.1&nbsp;SQL Query</h3></div></div></div><p>Cloud Spanner has SQL support for running read-only queries.
All the query related methods start with <code class="literal">query</code> on <code class="literal">SpannerTemplate</code>.
Using <code class="literal">SpannerTemplate</code> you can execute SQL queries that map to POJOs:</p><pre class="programlisting">List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.query(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, Statement.of(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"SELECT * FROM trades"</span>));</pre></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_read" href="#_read"></a>158.3.2&nbsp;Read</h3></div></div></div><p>Spanner exposes a <a class="link" href="https://cloud.google.com/spanner/docs/reads" target="_top">Read API</a> for reading single row or multiple rows in a table or in a secondary index.</p><p>Using <code class="literal">SpannerTemplate</code> you can execute reads, for example:</p><pre class="programlisting">List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.readAll(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>);</pre><p>Main benefit of reads over queries is reading multiple rows of a certain pattern of keys is much easier using the features of the <a class="link" href="https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-spanner/src/main/java/com/google/cloud/spanner/KeySet.java" target="_top"><code class="literal">KeySet</code></a> class.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_advanced_reads" href="#_advanced_reads"></a>158.3.3&nbsp;Advanced reads</h3></div></div></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_stale_read" href="#_stale_read"></a>Stale read</h4></div></div></div><p>All reads and queries are <span class="strong"><strong>strong reads</strong></span> by default.
A <span class="strong"><strong>strong read</strong></span> is a read at a current timestamp and is guaranteed to see all data that has been committed up until the start of this read.
A <span class="strong"><strong>stale read</strong></span> on the other hand is read at a timestamp in the past.
Cloud Spanner allows you to determine how current the data should be when you read data.
With <code class="literal">SpannerTemplate</code> you can specify the <code class="literal">Timestamp</code> by setting it on <code class="literal">SpannerQueryOptions</code> or <code class="literal">SpannerReadOptions</code> to the appropriate read or query methods:</p><p>Reads:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// a read with options:</span>
SpannerReadOptions spannerReadOptions = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> SpannerReadOptions().setTimestamp(Timestamp.now());
List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.readAll(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, spannerReadOptions);</pre><p>Queries:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// a query with options:</span>
SpannerQueryOptions spannerQueryOptions = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> SpannerQueryOptions().setTimestamp(Timestamp.now());
List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.query(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, Statement.of(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"SELECT * FROM trades"</span>), spannerQueryOptions);</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_read_from_a_secondary_index" href="#_read_from_a_secondary_index"></a>Read from a secondary index</h4></div></div></div><p>Using a <a class="link" href="https://cloud.google.com/spanner/docs/secondary-indexes" target="_top">secondary index</a> is available for Reads via the Template API and it is also implicitly available via SQL for Queries.</p><p>The following shows how to read rows from a table using a <a class="link" href="https://cloud.google.com/spanner/docs/secondary-indexes" target="_top">secondary index</a> simply by setting <code class="literal">index</code> on <code class="literal">SpannerReadOptions</code>:</p><pre class="programlisting">SpannerReadOptions spannerReadOptions = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> SpannerReadOptions().setIndex(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"TradesByTrader"</span>);
List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.readAll(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, spannerReadOptions);</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_read_with_offsets_and_limits" href="#_read_with_offsets_and_limits"></a>Read with offsets and limits</h4></div></div></div><p>Limits and offsets are only supported by Queries.
The following will get only the first two rows of the query:</p><pre class="programlisting">SpannerQueryOptions spannerQueryOptions = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> SpannerQueryOptions().setLimit(<span class="hl-number">2</span>).setOffset(<span class="hl-number">3</span>);
List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.query(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, Statement.of(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"SELECT * FROM trades"</span>), spannerQueryOptions);</pre><p>Note that the above is equivalent of executing <code class="literal">SELECT * FROM trades LIMIT 2 OFFSET 3</code>.</p></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_sorting" href="#_sorting"></a>Sorting</h4></div></div></div><p>Reads by keys do not support sorting.
However, queries on the Template API support sorting through standard SQL and also via Spring Data Sort API:</p><pre class="programlisting">List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.queryAll(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, Sort.by(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"action"</span>));</pre><p>If the provided sorted field name is that of a property of the domain type, then the column name corresponding to that property will be used in the query.
Otherwise, the given field name is assumed to be the name of the column in the Cloud Spanner table.
Sorting on columns of Cloud Spanner types STRING and BYTES can be done while ignoring case:</p><pre class="programlisting">Sort.by(Order.desc(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"action"</span>).ignoreCase())</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_partial_read" href="#_partial_read"></a>Partial read</h4></div></div></div><p>Partial read is only possible when using Queries.
In case the rows returned by the query have fewer columns than the entity that it will be mapped to, Spring Data will map the returned columns only.
This setting also applies to nested structs and their corresponding nested POJO properties.</p><pre class="programlisting">List&lt;Trade&gt; trades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.query(Trade.<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span>, Statement.of(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"SELECT action, symbol FROM trades"</span>),
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> SpannerQueryOptions().setAllowMissingResultSetColumns(true));</pre><p>If the setting is set to <code class="literal">false</code>, then an exception will be thrown if there are missing columns in the query result.</p></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_summary_of_options_for_query_vs_read" href="#_summary_of_options_for_query_vs_read"></a>Summary of options for Query vs Read</h4></div></div></div><div class="informaltable"><table class="informaltable" style="border-collapse: collapse;border-top: 1px solid ; border-bottom: 1px solid ; "><colgroup><col class="col_1"><col class="col_2"><col class="col_3"></colgroup><tbody><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Feature</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Query supports it</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>Read supports it</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>SQL</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>no</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Partial read</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>no</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Limits</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>no</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Offsets</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>no</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Secondary index</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td></tr><tr><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>Read using index range</p></td><td style="border-right: 1px solid ; border-bottom: 1px solid ; " align="left" valign="top"><p>no</p></td><td style="border-bottom: 1px solid ; " align="left" valign="top"><p>yes</p></td></tr><tr><td style="border-right: 1px solid ; " align="left" valign="top"><p>Sorting</p></td><td style="border-right: 1px solid ; " align="left" valign="top"><p>yes</p></td><td style="" align="left" valign="top"><p>no</p></td></tr></tbody></table></div></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_write_update" href="#_write_update"></a>158.3.4&nbsp;Write / Update</h3></div></div></div><p>The write methods of <code class="literal">SpannerOperations</code> accept a POJO and writes all of its properties to Spanner.
The corresponding Spanner table and entity metadata is obtained from the given object&#8217;s actual type.</p><p>If a POJO was retrieved from Spanner and its primary key properties values were changed and then written or updated, the operation will occur as if against a row with the new primary key values.
The row with the original primary key values will not be affected.</p><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_insert" href="#_insert"></a>Insert</h4></div></div></div><p>The <code class="literal">insert</code> method of <code class="literal">SpannerOperations</code> accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if a row with the POJO&#8217;s primary key already exists in the table.</p><pre class="programlisting">Trade t = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> Trade();
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.insert(t);</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_update" href="#_update"></a>Update</h4></div></div></div><p>The <code class="literal">update</code> method of <code class="literal">SpannerOperations</code> accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if the POJO&#8217;s primary key does not already exist in the table.</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// t was retrieved from a previous operation</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.update(t);</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_upsert" href="#_upsert"></a>Upsert</h4></div></div></div><p>The <code class="literal">upsert</code> method of <code class="literal">SpannerOperations</code> accepts a POJO and writes all of its properties to Spanner using update-or-insert.</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// t was retrieved from a previous operation or it's new</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.upsert(t);</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_partial_update" href="#_partial_update"></a>Partial Update</h4></div></div></div><p>The update methods of <code class="literal">SpannerOperations</code> operate by default on all properties within the given object, but also accept <code class="literal">String[]</code> and <code class="literal">Optional&lt;Set&lt;String&gt;&gt;</code> of column names.
If the <code class="literal">Optional</code> of set of column names is empty, then all columns are written to Spanner.
However, if the Optional is occupied by an empty set, then no columns will be written.</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// t was retrieved from a previous operation or it's new</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.update(t, <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"symbol"</span>, <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"action"</span>);</pre></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_dml" href="#_dml"></a>158.3.5&nbsp;DML</h3></div></div></div><p>DML statements can be executed using <code class="literal">SpannerOperations.executeDmlStatement</code>.
Inserts, updates, and deletions can affect any number of rows and entities.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_transactions" href="#_transactions"></a>158.3.6&nbsp;Transactions</h3></div></div></div><p><code class="literal">SpannerOperations</code> provides methods to run <code class="literal">java.util.Function</code> objects within a single transaction while making available the read and write methods from <code class="literal">SpannerOperations</code>.</p><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_readwrite_transaction" href="#_readwrite_transaction"></a>Read/Write Transaction</h4></div></div></div><p>Read and write transactions are provided by <code class="literal">SpannerOperations</code> via the <code class="literal">performReadWriteTransaction</code> method:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerOperations mySpannerOperations;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String doWorkInsideTransaction() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> mySpannerOperations.performReadWriteTransaction(
transActionSpannerOperations -&gt; {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Work with transActionSpannerOperations here.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// It is also a SpannerOperations object.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"transaction completed"</span>;
}
);
}</pre><p>The <code class="literal">performReadWriteTransaction</code> method accepts a <code class="literal">Function</code> that is provided an instance of a <code class="literal">SpannerOperations</code> object.
The final returned value and type of the function is determined by the user.
You can use this object just as you would a regular <code class="literal">SpannerOperations</code> with a few exceptions:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">Its read functionality cannot perform stale reads, because all reads and writes happen at the single point in time of the transaction.</li><li class="listitem">It cannot perform sub-transactions via <code class="literal">performReadWriteTransaction</code> or <code class="literal">performReadOnlyTransaction</code>.</li></ul></div><p>As these read-write transactions are locking, it is recommended that you use the <code class="literal">performReadOnlyTransaction</code> if your function does not perform any writes.</p></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_read_only_transaction" href="#_read_only_transaction"></a>Read-only Transaction</h4></div></div></div><p>The <code class="literal">performReadOnlyTransaction</code> method is used to perform read-only transactions using a <code class="literal">SpannerOperations</code>:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerOperations mySpannerOperations;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String doWorkInsideTransaction() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> mySpannerOperations.performReadOnlyTransaction(
transActionSpannerOperations -&gt; {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Work with transActionSpannerOperations here.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// It is also a SpannerOperations object.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"transaction completed"</span>;
}
);
}</pre><p>The <code class="literal">performReadOnlyTransaction</code> method accepts a <code class="literal">Function</code> that is provided an instance of a
<code class="literal">SpannerOperations</code> object.
This method also accepts a <code class="literal">ReadOptions</code> object, but the only attribute used is the timestamp used to determine the snapshot in time to perform the reads in the transaction.
If the timestamp is not set in the read options the transaction is run against the current state of the database.
The final returned value and type of the function is determined by the user.
You can use this object just as you would a regular <code class="literal">SpannerOperations</code> with
a few exceptions:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction.</li><li class="listitem">It cannot perform sub-transactions via <code class="literal">performReadWriteTransaction</code> or <code class="literal">performReadOnlyTransaction</code></li><li class="listitem">It cannot perform any write operations.</li></ul></div><p>Because read-only transactions are non-locking and can be performed on points in time in the past, these are recommended for functions that do not perform write operations.</p></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_declarative_transactions_with_transactional_annotation" href="#_declarative_transactions_with_transactional_annotation"></a>Declarative Transactions with @Transactional Annotation</h4></div></div></div><p>This feature requires a bean of <code class="literal">SpannerTransactionManager</code>, which is provided when using <code class="literal">spring-cloud-gcp-starter-data-spanner</code>.</p><p><code class="literal">SpannerTemplate</code> and <code class="literal">SpannerRepository</code> support running methods with the <code class="literal">@Transactional</code> [annotation](<a class="link" href="https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative" target="_top">https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative</a>) as transactions.
If a method annotated with <code class="literal">@Transactional</code> calls another method also annotated, then both methods will work within the same transaction.
<code class="literal">performReadOnlyTransaction</code> and <code class="literal">performReadWriteTransaction</code> cannot be used in <code class="literal">@Transactional</code> annotated methods because Cloud Spanner does not support transactions within transactions.</p></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_dml_statements" href="#_dml_statements"></a>158.3.7&nbsp;DML Statements</h3></div></div></div><p><code class="literal">SpannerTemplate</code> supports [DML](<a class="link" href="https://cloud.google.com/spanner/docs/dml-tasks" target="_top">https://cloud.google.com/spanner/docs/dml-tasks</a>) <code class="literal">Statements</code>.
DML statements can be executed in transactions via <code class="literal">performReadWriteTransaction</code> or using the <code class="literal">@Transactional</code> annotation.</p><p>When DML statements are executed outside of transactions, they are executed in [partitioned-mode](<a class="link" href="https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml" target="_top">https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml</a>).</p></div></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_repositories" href="#_repositories"></a>158.4&nbsp;Repositories</h2></div></div></div><p><a class="link" href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories" target="_top">Spring Data Repositories</a> are a powerful abstraction that can save you a lot of boilerplate code.</p><p>For example:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TraderRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trader, String&gt; {
}</pre><p>Spring Data generates a working implementation of the specified interface, which can be conveniently autowired into an application.</p><p>The <code class="literal">Trader</code> type parameter to <code class="literal">SpannerRepository</code> refers to the underlying domain type.
The second type parameter, <code class="literal">String</code> in this case, refers to the type of the key of the domain type.</p><p>For POJOs with a composite primary key, this ID type parameter can be any descendant of <code class="literal">Object[]</code> compatible with all primary key properties, any descendant of <code class="literal">Iterable</code>, or <code class="literal">com.google.cloud.spanner.Key</code>.
If the domain POJO type only has a single primary key column, then the primary key property type can be used or the <code class="literal">Key</code> type.</p><p>For example in case of Trades, that belong to a Trader, <code class="literal">TradeRepository</code> would look like this:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, String[]&gt; {
}</pre><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> MyApplication {
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerTemplate spannerTemplate;
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
StudentRepository studentRepository;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">void</span> demo() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.tradeRepository.deleteAll();
String traderId = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"demo_trader"</span>;
Trade t = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">new</span> Trade();
t.symbol = stock;
t.action = action;
t.traderId = traderId;
t.price = <span class="hl-number">100.0</span>;
t.shares = <span class="hl-number">12345.6</span>;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.spannerTemplate.insert(t);
Iterable&lt;Trade&gt; allTrades = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.tradeRepository.findAll();
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">int</span> count = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">this</span>.tradeRepository.countByAction(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"BUY"</span>);
}
}</pre><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_crud_repository" href="#_crud_repository"></a>158.4.1&nbsp;CRUD Repository</h3></div></div></div><p><code class="literal">CrudRepository</code> methods work as expected, with one thing Spanner specific: the <code class="literal">save</code> and <code class="literal">saveAll</code> methods work as update-or-insert.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_paging_and_sorting_repository" href="#_paging_and_sorting_repository"></a>158.4.2&nbsp;Paging and Sorting Repository</h3></div></div></div><p>You can also use <code class="literal">PagingAndSortingRepository</code> with Spanner Spring Data.
The sorting and pageable <code class="literal">findAll</code> methods available from this interface operate on the current state of the Spanner database.
As a result, beware that the state of the database (and the results) might change when moving page to page.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_spanner_repository" href="#_spanner_repository"></a>158.4.3&nbsp;Spanner Repository</h3></div></div></div><p>The <code class="literal">SpannerRepository</code> extends the <code class="literal">PagingAndSortingRepository</code>, but adds the read-only and the read-write transaction functionality provided by Spanner.
These transactions work very similarly to those of <code class="literal">SpannerOperations</code>, but is specific to the repository&#8217;s domain type and provides repository functions instead of template functions.</p><p>For example, this is a read-write transaction:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
SpannerRepository myRepo;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> String doWorkInsideTransaction() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> myRepo.performReadOnlyTransaction(
transactionSpannerRepo -&gt; {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Work with the single-transaction transactionSpannerRepo here.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// This is a SpannerRepository object.</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"transaction completed"</span>;
}
);
}</pre><p>When creating custom repositories for your own domain types and query methods, you can extend <code class="literal">SpannerRepository</code> to access Cloud Spanner-specific features as well as all features from <code class="literal">PagingAndSortingRepository</code> and <code class="literal">CrudRepository</code>.</p></div></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_query_methods" href="#_query_methods"></a>158.5&nbsp;Query Methods</h2></div></div></div><p><code class="literal">SpannerRepository</code> supports Query Methods.
Described in the following sections, these are methods residing in your custom repository interfaces of which implementations are generated based on their names and annotations.
Query Methods can read, write, and delete entities in Cloud Spanner.
Parameters to these methods can be any Cloud Spanner data type supported directly or via custom configured converters.
Parameters can also be of type <code class="literal">Struct</code> or POJOs.
If a POJO is given as a parameter, it will be converted to a <code class="literal">Struct</code> with the same type-conversion logic as used to create write mutations.
Comparisons using Struct parameters are limited to <a class="link" href="https://cloud.google.com/spanner/docs/data-types#limited-comparisons-for-struct" target="_top">what is available with Cloud Spanner</a>.</p><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_query_methods_by_convention" href="#_query_methods_by_convention"></a>158.5.1&nbsp;Query methods by convention</h3></div></div></div><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, String[]&gt; {
List&lt;Trade&gt; findByAction(String action);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">int</span> countByAction(String action);
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// Named methods are powerful, but can get unwieldy</span>
List&lt;Trade&gt; findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(
String action, String symbol, String traderId);
}</pre><p>In the example above, the <a class="link" href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories.query-methods" target="_top">query methods</a> in <code class="literal">TradeRepository</code> are generated based on the name of the methods, using the <a class="link" href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation" target="_top">Spring Data Query creation naming convention</a>.</p><p><code class="literal">List&lt;Trade&gt; findByAction(String action)</code> would translate to a <code class="literal">SELECT * FROM trades WHERE action = ?</code>.</p><p>The function <code class="literal">List&lt;Trade&gt; findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId);</code> will be translated as the equivalent of this SQL query:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">SELECT</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">DISTINCT</span> * <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">FROM</span> trades
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">WHERE</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">ACTION</span> = ? <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">AND</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">LOWER</span>(SYMBOL) = <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">LOWER</span>(?) <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">AND</span> TRADER_ID = ?
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">ORDER</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">BY</span> SYMBOL <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">DESC</span>
LIMIT <span class="hl-number">3</span></pre><p>The following filter options are supported:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem">Equality</li><li class="listitem">Greater than or equals</li><li class="listitem">Greater than</li><li class="listitem">Less than or equals</li><li class="listitem">Less than</li><li class="listitem">Is null</li><li class="listitem">Is not null</li><li class="listitem">Is true</li><li class="listitem">Is false</li><li class="listitem">Like a string</li><li class="listitem">Not like a string</li><li class="listitem">Contains a string</li><li class="listitem">Not contains a string</li></ul></div><p>Note that the phrase <code class="literal">SymbolIgnoreCase</code> is translated to <code class="literal">LOWER(SYMBOL) = LOWER(?)</code> indicating a non-case-sensitive matching.
The <code class="literal">IgnoreCase</code> phrase may only be appended to fields that correspond to columns of type STRING or BYTES.
The Spring Data "AllIgnoreCase" phrase appended at the end of the method name is not supported.</p><p>The <code class="literal">Like</code> or <code class="literal">NotLike</code> naming conventions:</p><pre class="programlisting">List&lt;Trade&gt; findBySymbolLike(String symbolFragment);</pre><p>The param <code class="literal">symbolFragment</code> can contain <a class="link" href="https://cloud.google.com/spanner/docs/functions-and-operators#comparison-operators" target="_top">wildcard characters</a> for string matching such as <code class="literal">_</code> and <code class="literal">%</code>.</p><p>The <code class="literal">Contains</code> and <code class="literal">NotContains</code> naming conventions:</p><pre class="programlisting">List&lt;Trade&gt; findBySymbolContains(String symbolFragment);</pre><p>The param <code class="literal">symbolFragment</code> is a <a class="link" href="https://cloud.google.com/spanner/docs/functions-and-operators#regexp_contains" target="_top">regular expression</a> that is checked for occurrences.</p><p>Delete queries are also supported.
For example, query methods such as <code class="literal">deleteByAction</code> or <code class="literal">removeByAction</code> delete entities found by <code class="literal">findByAction</code>.
The delete operation happens in a single transaction.</p><p>Delete queries can have the following return types:
* An integer type that is the number of entities deleted
* A collection of entities that were deleted
* <code class="literal">void</code></p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_custom_sqldml_query_methods" href="#_custom_sqldml_query_methods"></a>158.5.2&nbsp;Custom SQL/DML query methods</h3></div></div></div><p>The example above for <code class="literal">List&lt;Trade&gt; fetchByActionNamedQuery(String action)</code> does not match the <a class="link" href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation" target="_top">Spring Data Query creation naming convention</a>, so we have to map a parametrized Spanner SQL query to it.</p><p>The SQL query for the method can be mapped to repository methods in one of two ways:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><code class="literal">namedQueries</code> properties file</li><li class="listitem">using the <code class="literal">@Query</code> annotation</li></ul></div><p>The names of the tags of the SQL correspond to the <code class="literal">@Param</code> annotated names of the method parameters.</p><p>Custom SQL query methods can accept a single <code class="literal">Sort</code> or <code class="literal">Pageable</code> parameter that is applied on top of any sorting or paging in the SQL:</p><pre class="programlisting"> <em><span class="hl-annotation" style="color: gray">@Query("SELECT * FROM trades ORDER BY action DESC")</span></em>
List&lt;Trade&gt; sortedTrades(Pageable pageable);
<em><span class="hl-annotation" style="color: gray">@Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1")</span></em>
Trade sortedTopTrade(Pageable pageable);</pre><p>This can be used:</p><pre class="programlisting"> List&lt;Trade&gt; customSortedTrades = tradeRepository.sortedTrades(PageRequest
.of(<span class="hl-number">2</span>, <span class="hl-number">2</span>, org.springframework.data.domain.Sort.by(Order.asc(<span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">"id"</span>))));</pre><p>The results would be sorted by "id" in ascending order.</p><p>Your query method can also return non-entity types:</p><pre class="programlisting"> <em><span class="hl-annotation" style="color: gray">@Query("SELECT COUNT(1) FROM trades WHERE action = @action")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">int</span> countByActionQuery(String action);
<em><span class="hl-annotation" style="color: gray">@Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">boolean</span> existsByActionQuery(String action);
<em><span class="hl-annotation" style="color: gray">@Query("SELECT action FROM trades WHERE action = @action LIMIT 1")</span></em>
String getFirstString(<em><span class="hl-annotation" style="color: gray">@Param("action")</span></em> String action);
<em><span class="hl-annotation" style="color: gray">@Query("SELECT action FROM trades WHERE action = @action")</span></em>
List&lt;String&gt; getFirstStringList(<em><span class="hl-annotation" style="color: gray">@Param("action")</span></em> String action);</pre><p>DML statements can also be executed by query methods, but the only possible return value is a <code class="literal">long</code> representing the number of affected rows.
The <code class="literal">dmlStatement</code> boolean setting must be set on <code class="literal">@Query</code> to indicate that the query method is executed as a DML statement.</p><pre class="programlisting"> <em><span class="hl-annotation" style="color: gray">@Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true)</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">long</span> deleteByActionQuery(String action);</pre><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_query_methods_with_named_queries_properties" href="#_query_methods_with_named_queries_properties"></a>Query methods with named queries properties</h4></div></div></div><p>By default, the <code class="literal">namedQueriesLocation</code> attribute on <code class="literal">@EnableSpannerRepositories</code> points to the <code class="literal">META-INF/spanner-named-queries.properties</code> file.
You can specify the query for a method in the properties file by providing the SQL as the value for the "interface.method" property:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-attribute">Trade.fetchByActionNamedQuery</span>=SELECT * FROM trades WHERE trades.action = @tag0</pre><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, String[]&gt; {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// This method uses the query from the properties file instead of one generated based on name.</span>
List&lt;Trade&gt; fetchByActionNamedQuery(<em><span class="hl-annotation" style="color: gray">@Param("tag0")</span></em> String action);
}</pre></div><div class="section"><div class="titlepage"><div><div><h4 class="title"><a name="_query_methods_with_annotation" href="#_query_methods_with_annotation"></a>Query methods with annotation</h4></div></div></div><p>Using the <code class="literal">@Query</code> annotation:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, String[]&gt; {
<em><span class="hl-annotation" style="color: gray">@Query("SELECT * FROM trades WHERE trades.action = @tag0")</span></em>
List&lt;Trade&gt; fetchByActionNamedQuery(<em><span class="hl-annotation" style="color: gray">@Param("tag0")</span></em> String action);
}</pre><p>Table names can be used directly.
For example, "trades" in the above example.
Alternatively, table names can be resolved from the <code class="literal">@Table</code> annotation on domain classes as well.
In this case, the query should refer to table names with fully qualified class names between <code class="literal">:</code>
characters: <code class="literal">:fully.qualified.ClassName:</code>.
A full example would look like:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0")</span></em>
List&lt;Trade&gt; fetchByActionNamedQuery(String action);</pre><p>This allows table names evaluated with SpEL to be used in custom queries.</p><p>SpEL can also be used to provide SQL parameters:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0
AND price &gt; #{#priceRadius * -1} AND price &lt; #{#priceRadius * 2}")</span></em>
List&lt;Trade&gt; fetchByActionNamedQuery(String action, Double priceRadius);</pre></div></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_projections" href="#_projections"></a>158.5.3&nbsp;Projections</h3></div></div></div><p>Spring Data Spanner supports <a class="link" href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#projections" target="_top">projections</a>.
You can define projection interfaces based on domain types and add query methods that return them in your repository:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeProjection {
String getAction();
<em><span class="hl-annotation" style="color: gray">@Value("#{target.symbol + ' ' + target.action}")</span></em>
String getSymbolAndAction();
}
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, Key&gt; {
List&lt;Trade&gt; findByTraderId(String traderId);
List&lt;TradeProjection&gt; findByAction(String action);
<em><span class="hl-annotation" style="color: gray">@Query("SELECT action, symbol FROM trades WHERE action = @action")</span></em>
List&lt;TradeProjection&gt; findByQuery(String action);
}</pre><p>Projections can be provided by name-convention-based query methods as well as by custom SQL queries.
If using custom SQL queries, you can further restrict the columns retrieved from Spanner to just those required by the projection to improve performance.</p><p>Properties of projection types defined using SpEL use the fixed name <code class="literal">target</code> for the underlying domain object.
As a result accessing underlying properties take the form <code class="literal">target.&lt;property-name&gt;</code>.</p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="_rest_repositories" href="#_rest_repositories"></a>158.5.4&nbsp;REST Repositories</h3></div></div></div><p>When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:</p><pre class="programlisting"><span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;dependency&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;groupId&gt;</span>org.springframework.boot<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/groupId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;artifactId&gt;</span>spring-boot-starter-data-rest<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/artifactId&gt;</span>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-tag">&lt;/dependency&gt;</span></pre><p>If you prefer to configure parameters (such as path), you can use <code class="literal">@RepositoryRestResource</code> annotation:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@RepositoryRestResource(collectionResourceRel = "trades", path = "trades")</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">interface</span> TradeRepository <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerRepository&lt;Trade, String[]&gt; {
}</pre><p>For example, you can retrieve all <code class="literal">Trade</code> objects in the repository by using <code class="literal">curl http://&lt;server&gt;:&lt;port&gt;/trades</code>, or any specific trade via <code class="literal">curl http://&lt;server&gt;:&lt;port&gt;/trades/&lt;trader_id&gt;,&lt;trade_id&gt;</code>.</p><p>The separator between your primary key components, <code class="literal">id</code> and <code class="literal">trader_id</code> in this case, is a comma by default, but can be configured to any string not found in your key values by extending the <code class="literal">SpannerKeyIdConverter</code> class:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Component</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">class</span> MySpecialIdConverter <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">extends</span> SpannerKeyIdConverter {
<em><span class="hl-annotation" style="color: gray">@Override</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">protected</span> String getUrlIdSeparator() {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">return</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-string">":"</span>;
}
}</pre><p>You can also write trades using <code class="literal">curl -XPOST -H"Content-Type: application/json" -<a class="link" href="mailto:d@test.json" target="_top">d@test.json</a> http://&lt;server&gt;:&lt;port&gt;/trades/</code> where the file <code class="literal">test.json</code> holds the JSON representation of a <code class="literal">Trade</code> object.</p></div></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_database_and_schema_admin" href="#_database_and_schema_admin"></a>158.6&nbsp;Database and Schema Admin</h2></div></div></div><p>Databases and tables inside Spanner instances can be created automatically from <code class="literal">SpannerPersistentEntity</code> objects:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> SpannerSchemaUtils spannerSchemaUtils;
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">private</span> SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate;
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">public</span> <span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">void</span> createTable(SpannerPersistentEntity entity) {
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-keyword">if</span>(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){
<span xmlns:d="http://docbook.org/ns/docbook" class="hl-comment">// The boolean parameter indicates that the database will be created if it does not exist.</span>
spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList(
spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true);
}
}</pre><p>Schemas can be generated for entire object hierarchies with interleaved relationships and composite keys.</p></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="_sample_9" href="#_sample_9"></a>158.7&nbsp;Sample</h2></div></div></div><p>A <a class="link" href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample" target="_top">sample application</a> is available.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="multi__spring_cloud_config_2.html">Prev</a>&nbsp;</td><td width="20%" align="center"><a accesskey="u" href="multi_spring-cloud-gcp-reference.html">Up</a></td><td width="40%" align="right">&nbsp;<a accesskey="n" href="multi__spring_data_cloud_datastore.html">Next</a></td></tr><tr><td width="40%" align="left" valign="top">157.&nbsp;Spring Cloud Config&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="multi_spring-cloud.html">Home</a></td><td width="40%" align="right" valign="top">&nbsp;159.&nbsp;Spring Data Cloud Datastore</td></tr></table></div></body></html>