4281 lines
261 KiB
XML
4281 lines
261 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
||
<?asciidoc-toc?>
|
||
<?asciidoc-numbered?>
|
||
<book xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0" xml:lang="en">
|
||
<info>
|
||
<title>Spring Cloud GCP</title>
|
||
<date>2019-01-22</date>
|
||
<authorgroup>
|
||
<author>
|
||
<personname>
|
||
<firstname>João</firstname>
|
||
<othername>André</othername>
|
||
<surname>Martins</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Jisha</firstname>
|
||
<surname>Abubaker</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Ray</firstname>
|
||
<surname>Tsang</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Mike</firstname>
|
||
<surname>Eltsufin</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Artem</firstname>
|
||
<surname>Bilan</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Andreas</firstname>
|
||
<surname>Berger</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Balint</firstname>
|
||
<surname>Pato</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Chengyuan</firstname>
|
||
<surname>Zhao</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Dmitry</firstname>
|
||
<surname>Solomakha</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Elena</firstname>
|
||
<surname>Felder</surname>
|
||
</personname>
|
||
</author>
|
||
<author>
|
||
<personname>
|
||
<firstname>Daniel</firstname>
|
||
<surname>Zou</surname>
|
||
</personname>
|
||
</author>
|
||
</authorgroup>
|
||
</info>
|
||
<chapter xml:id="_introduction">
|
||
<title>Introduction</title>
|
||
<simpara>The Spring Cloud GCP project makes the Spring Framework a first-class citizen of Google Cloud Platform (GCP).</simpara>
|
||
<simpara>Spring Cloud GCP lets you leverage the power and simplicity of the Spring Framework to:</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Analyze your images for text, objects, and other content with Google Cloud Vision</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Use Spring Security via Google Cloud IAP</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Map objects, relationships, and collections with Spring Data Cloud Spanner and Spring Data Cloud Datastore</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Publish and subscribe to Google Cloud Pub/Sub topics</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Configure Spring JDBC with a few properties to use Google Cloud SQL</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Write and read from Spring Resources backed up by Google Cloud Storage</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Exchange messages with Spring Integration using Google Cloud Pub/Sub on the background</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Trace the execution of your app with Spring Cloud Sleuth and Google Stackdriver Trace</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Configure your app with Spring Cloud Config, backed up by the Google Runtime Configuration API</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Consume and produce Google Cloud Storage data via Spring Integration GCS Channel Adapters</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
</chapter>
|
||
<chapter xml:id="_dependency_management">
|
||
<title>Dependency Management</title>
|
||
<simpara>The Spring Cloud GCP Bill of Materials (BOM) contains the versions of all the dependencies it uses.</simpara>
|
||
<simpara>If you’re a Maven user, adding the following to your pom.xml file will allow you to not specify any Spring Cloud GCP dependency versions.
|
||
Instead, the version of the BOM you’re using determines the versions of the used dependencies.</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependencyManagement>
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-dependencies</artifactId>
|
||
<version>{project-version}</version>
|
||
<type>pom</type>
|
||
<scope>import</scope>
|
||
</dependency>
|
||
</dependencies>
|
||
</dependencyManagement></programlisting>
|
||
<simpara>In the following sections, it will be assumed you are using the Spring Cloud GCP BOM and the dependency snippets will not contain versions.</simpara>
|
||
<simpara>Gradle users can achieve the same kind of BOM experience using Spring’s <link xl:href="https://github.com/spring-gradle-plugins/dependency-management-plugin">dependency-management-plugin</link> Gradle plugin.
|
||
For simplicity, the Gradle dependency snippets in the remainder of this document will also omit their versions.</simpara>
|
||
</chapter>
|
||
<chapter xml:id="_getting_started">
|
||
<title>Getting started</title>
|
||
<simpara>There are many available resources to get you up to speed with our libraries as quickly as possible.</simpara>
|
||
<section xml:id="_spring_initializr">
|
||
<title>Spring Initializr</title>
|
||
<simpara>There are three entries in <link xl:href="http://start.spring.io/">Spring Initializr</link> for Spring Cloud GCP.</simpara>
|
||
<section xml:id="_gcp_support">
|
||
<title>GCP Support</title>
|
||
<simpara>The GCP Support entry contains auto-configuration support for every Spring Cloud GCP integration.
|
||
Most of the autoconfiguration code is only enabled if other dependencies are added to the classpath.</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="2">
|
||
<colspec colname="col_1" colwidth="50*"/>
|
||
<colspec colname="col_2" colwidth="50*"/>
|
||
<thead>
|
||
<row>
|
||
<entry align="left" valign="top">Spring Cloud GCP Starter</entry>
|
||
<entry align="left" valign="top">Required dependencies</entry>
|
||
</row>
|
||
</thead>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Config</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-config</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Cloud Spanner</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-data-spanner</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Cloud Datastore</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-data-datastore</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Logging</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-logging</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>SQL - MySql</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-sql-mysql</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>SQL - PostgreSQL</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-sql-postgres</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Trace</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-trace</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Vision</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-vision</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Security - IAP</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>org.springframework.cloud:spring-cloud-gcp-starter-security-iap</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_gcp_messaging">
|
||
<title>GCP Messaging</title>
|
||
<simpara>The GCP Messaging entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Pub/Sub integrations work out of the box.</simpara>
|
||
</section>
|
||
<section xml:id="_gcp_storage">
|
||
<title>GCP Storage</title>
|
||
<simpara>The GCP Storage entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Storage integrations work out of the box.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_code_samples">
|
||
<title>Code Samples</title>
|
||
<simpara>There are <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples">code samples</link> available that demonstrate the usage of all our integrations.</simpara>
|
||
<simpara>For example, <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-vision-api-sample">the Vision API sample</link> shows how to use <literal>spring-cloud-gcp-starter-vision</literal> to automatically configure Vision API clients.</simpara>
|
||
</section>
|
||
<section xml:id="_code_challenges">
|
||
<title>Code Challenges</title>
|
||
<simpara>In a code challenge, you perform a task step by step, using one integration.
|
||
There are a number of challenges available in the <link xl:href="https://codelabs.developers.google.com/spring">Google Developers Codelabs</link> page.</simpara>
|
||
</section>
|
||
<section xml:id="_getting_started_guides">
|
||
<title>Getting Started Guides</title>
|
||
<simpara>A Spring Getting Started guide on messaging with Spring Integration Channel Adapters for Google Cloud Pub/Sub is available from <link xl:href="https://spring.io/guides/gs/messaging-gcp-pubsub/">Spring Guides</link>.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="spring-cloud-gcp-core">
|
||
<title>Spring Cloud GCP Core</title>
|
||
<simpara>Each Spring Cloud GCP module uses <literal>GcpProjectIdProvider</literal> and <literal>CredentialsProvider</literal> to get the GCP project ID and access credentials.</simpara>
|
||
<simpara>Spring Cloud GCP provides a Spring Boot starter to auto-configure the core components.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter'
|
||
}</screen>
|
||
<section xml:id="_project_id">
|
||
<title>Project ID</title>
|
||
<simpara><literal>GcpProjectIdProvider</literal> is a functional interface that returns a GCP project ID string.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface GcpProjectIdProvider {
|
||
String getProjectId();
|
||
}</programlisting>
|
||
<simpara>The Spring Cloud GCP starter auto-configures a <literal>GcpProjectIdProvider</literal>.
|
||
If a <literal>spring.cloud.gcp.project-id</literal> property is specified, the provided <literal>GcpProjectIdProvider</literal> returns that property value.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">spring.cloud.gcp.project-id=my-gcp-project-id</programlisting>
|
||
<simpara>Otherwise, the project ID is discovered based on an
|
||
<link xl:href="https://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/ServiceOptions.html#getDefaultProjectId--">ordered list of rules</link>:</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>The project ID specified by the <literal>GOOGLE_CLOUD_PROJECT</literal> environment variable</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The Google App Engine project ID</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The project ID specified in the JSON credentials file pointed by the <literal>GOOGLE_APPLICATION_CREDENTIALS</literal> environment variable</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The Google Cloud SDK project ID</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The Google Compute Engine project ID, from the Google Compute Engine Metadata Server</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
</section>
|
||
<section xml:id="_credentials">
|
||
<title>Credentials</title>
|
||
<simpara><literal>CredentialsProvider</literal> is a functional interface that returns the credentials to authenticate and authorize calls to Google Cloud Client Libraries.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface CredentialsProvider {
|
||
Credentials getCredentials() throws IOException;
|
||
}</programlisting>
|
||
<simpara>The Spring Cloud GCP starter auto-configures a <literal>CredentialsProvider</literal>.
|
||
It uses the <literal>spring.cloud.gcp.credentials.location</literal> property to locate the OAuth2 private key of a Google service account.
|
||
Keep in mind this property is a Spring Resource, so the credentials file can be obtained from a number of <link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/html/resources.html#resources-implementations">different locations</link> such as the file system, classpath, URL, etc.
|
||
The next example specifies the credentials location property in the file system.</simpara>
|
||
<screen>spring.cloud.gcp.credentials.location=file:/usr/local/key.json</screen>
|
||
<simpara>Alternatively, you can set the credentials by directly specifying the <literal>spring.cloud.gcp.credentials.encoded-key</literal> property.
|
||
The value should be the base64-encoded account private key in JSON format.</simpara>
|
||
<simpara>If that credentials aren’t specified through properties, the starter tries to discover credentials from a <link xl:href="https://github.com/GoogleCloudPlatform/google-cloud-java#authentication">number of places</link>:</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Credentials file pointed to by the <literal>GOOGLE_APPLICATION_CREDENTIALS</literal> environment variable</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Credentials provided by the Google Cloud SDK <literal>gcloud auth application-default login</literal> command</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Google App Engine built-in credentials</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Google Cloud Shell built-in credentials</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Google Compute Engine built-in credentials</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
<simpara>If your app is running on Google App Engine or Google Compute Engine, in most cases, you should omit the <literal>spring.cloud.gcp.credentials.location</literal> property and, instead, let the Spring Cloud GCP Starter get the correct credentials for those environments.
|
||
On App Engine Standard, the <link xl:href="https://cloud.google.com/appengine/docs/standard/java/appidentity/">App Identity service account credentials</link> are used, on App Engine Flexible, the <link xl:href="https://cloud.google.com/appengine/docs/flexible/java/service-account">Flexible service account credential</link> are used and on Google Compute Engine, the <link xl:href="https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using_the_compute_engine_default_service_account">Compute Engine Default Service Account</link> is used.</simpara>
|
||
<section xml:id="_scopes">
|
||
<title>Scopes</title>
|
||
<simpara>By default, the credentials provided by the Spring Cloud GCP Starter contain scopes for every service supported by Spring Cloud GCP.</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="2">
|
||
<colspec colname="col_1" colwidth="50*"/>
|
||
<colspec colname="col_2" colwidth="50*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Service</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Scope</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Spanner</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/spanner.admin">https://www.googleapis.com/auth/spanner.admin</link>, <link xl:href="https://www.googleapis.com/auth/spanner.data">https://www.googleapis.com/auth/spanner.data</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Datastore</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/datastore">https://www.googleapis.com/auth/datastore</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Pub/Sub</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/pubsub">https://www.googleapis.com/auth/pubsub</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Storage (Read Only)</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/devstorage.read_only">https://www.googleapis.com/auth/devstorage.read_only</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Storage (Write/Write)</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/devstorage.read_write">https://www.googleapis.com/auth/devstorage.read_write</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Runtime Config</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/cloudruntimeconfig">https://www.googleapis.com/auth/cloudruntimeconfig</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Trace (Append)</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/trace.append">https://www.googleapis.com/auth/trace.append</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Cloud Platform</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/cloud-platform">https://www.googleapis.com/auth/cloud-platform</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Vision</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/cloud-vision">https://www.googleapis.com/auth/cloud-vision</link></simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<simpara>The Spring Cloud GCP starter allows you to configure a custom scope list for the provided credentials.
|
||
To do that, specify a comma-delimited list of <link xl:href="https://developers.google.com/identity/protocols/googlescopes">Google OAuth2 scopes</link> in the <literal>spring.cloud.gcp.credentials.scopes</literal> property.</simpara>
|
||
<simpara><literal>spring.cloud.gcp.credentials.scopes</literal> is a comma-delimited list of <link xl:href="https://developers.google.com/identity/protocols/googlescopes">Google OAuth2 scopes</link> for Google Cloud Platform services that the credentials returned by the provided <literal>CredentialsProvider</literal> support.</simpara>
|
||
<screen>spring.cloud.gcp.credentials.scopes=https://www.googleapis.com/auth/pubsub,https://www.googleapis.com/auth/sqlservice.admin</screen>
|
||
<simpara>You can also use <literal>DEFAULT_SCOPES</literal> placeholder as a scope to represent the starters default scopes, and append the additional scopes you need to add.</simpara>
|
||
<screen>spring.cloud.gcp.credentials.scopes=DEFAULT_SCOPES,https://www.googleapis.com/auth/cloud-vision</screen>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_environment">
|
||
<title>Environment</title>
|
||
<simpara><literal>GcpEnvironmentProvider</literal> is a functional interface, auto-configured by the Spring Cloud GCP starter, that returns a <literal>GcpEnvironment</literal> enum.
|
||
The provider can help determine programmatically in which GCP environment (App Engine Flexible, App Engine Standard, Kubernetes Engine or Compute Engine) the application is deployed.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface GcpEnvironmentProvider {
|
||
GcpEnvironment getCurrentEnvironment();
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_spring_initializr_2">
|
||
<title>Spring Initializr</title>
|
||
<simpara>This starter is available from <link xl:href="http://start.spring.io/">Spring Initializr</link> through the <literal>GCP Support</literal> entry.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_google_cloud_pubsub">
|
||
<title>Google Cloud Pub/Sub</title>
|
||
<simpara>Spring Cloud GCP provides an abstraction layer to publish to and subscribe from Google Cloud Pub/Sub topics and to create, list or delete Google Cloud Pub/Sub topics and subscriptions.</simpara>
|
||
<simpara>A Spring Boot starter is provided to auto-configure the various required Pub/Sub components.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-pubsub'
|
||
}</screen>
|
||
<simpara>This starter is also available from <link xl:href="https://start.spring.io">Spring Initializr</link> through the <literal>GCP Messaging</literal> entry.</simpara>
|
||
<section xml:id="_pubsub_operations_template">
|
||
<title>Pub/Sub Operations & Template</title>
|
||
<simpara><literal>PubSubOperations</literal> is an abstraction that allows Spring users to use Google Cloud Pub/Sub without depending on any Google Cloud Pub/Sub API semantics.
|
||
It provides the common set of operations needed to interact with Google Cloud Pub/Sub.
|
||
<literal>PubSubTemplate</literal> is the default implementation of <literal>PubSubOperations</literal> and it uses the <link xl:href="https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-pubsub">Google Cloud Java Client for Pub/Sub</link> to interact with Google Cloud Pub/Sub.</simpara>
|
||
<simpara><literal>PubSubTemplate</literal> depends on a <literal>PublisherFactory</literal> and a <literal>SubscriberFactory</literal>.
|
||
The <literal>PublisherFactory</literal> provides a Google Cloud Java Client for Pub/Sub <literal>Publisher</literal>.
|
||
The <literal>SubscriberFactory</literal> provides the <literal>Subscriber</literal> for asynchronous message pulling, as well as a <literal>SubscriberStub</literal> for synchronous pulling.
|
||
The Spring Boot starter for GCP Pub/Sub auto-configures a <literal>PublisherFactory</literal> and <literal>SubscriberFactory</literal> with default settings and uses the <literal>GcpProjectIdProvider</literal> and <literal>CredentialsProvider</literal> auto-configured by the Spring Boot GCP starter.</simpara>
|
||
<simpara>The <literal>PublisherFactory</literal> implementation provided by Spring Cloud GCP Pub/Sub, <literal>DefaultPublisherFactory</literal>, caches <literal>Publisher</literal> instances by topic name, in order to optimize resource utilization.</simpara>
|
||
<simpara>The <literal>PubSubOperations</literal> interface is actually a combination of <literal>PubSubPublisherOperations</literal> and <literal>PubSubSubscriberOperations</literal> with the corresponding <literal>PubSubPublisherTemplate</literal> and <literal>PubSubSubscriberTemplate</literal> implementations, which can be used individually or via the composite <literal>PubSubTemplate</literal>.
|
||
The rest of the documentation refers to <literal>PubSubTemplate</literal>, but the same applies to <literal>PubSubPublisherTemplate</literal> and <literal>PubSubSubscriberTemplate</literal>, depending on whether we’re talking about publishing or subscribing.</simpara>
|
||
<section xml:id="_publishing_to_a_topic">
|
||
<title>Publishing to a topic</title>
|
||
<simpara><literal>PubSubTemplate</literal> provides asynchronous methods to publish messages to a Google Cloud Pub/Sub topic.
|
||
The <literal>publish()</literal> method takes in a topic name to post the message to, a payload of a generic type and, optionally, a map with the message headers.</simpara>
|
||
<simpara>Here is an example of how to publish a message to a Google Cloud Pub/Sub topic:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void publishMessage() {
|
||
this.pubSubTemplate.publish("topic", "your message payload", ImmutableMap.of("key1", "val1"));
|
||
}</programlisting>
|
||
<simpara>By default, the <literal>SimplePubSubMessageConverter</literal> is used to convert payloads of type <literal>byte[]</literal>, <literal>ByteString</literal>, <literal>ByteBuffer</literal>, and <literal>String</literal> to Pub/Sub messages.</simpara>
|
||
<section xml:id="_json_support">
|
||
<title>JSON support</title>
|
||
<simpara>For serialization and deserialization of POJOs using Jackson JSON, configure a <literal>JacksonPubSubMessageConverter</literal> bean, and the Spring Boot starter for GCP Pub/Sub will automatically wire it into the <literal>PubSubTemplate</literal>.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// Note: The ObjectMapper is used to convert Java POJOs to and from JSON.
|
||
// You will have to configure your own instance if you are unable to depend
|
||
// on the ObjectMapper provided by Spring Boot starters.
|
||
@Bean
|
||
public JacksonPubSubMessageConverter jacksonPubSubMessageConverter(ObjectMapper objectMapper) {
|
||
return new JacksonPubSubMessageConverter(objectMapper);
|
||
}</programlisting>
|
||
<simpara>Alternatively, you can set it directly by calling the <literal>setMessageConverter()</literal> method on the <literal>PubSubTemplate</literal>.
|
||
Other implementations of the <literal>PubSubMessageConverter</literal> can also be configured in the same manner.</simpara>
|
||
<simpara>Please refer to our <link xl:href="../spring-cloud-gcp-samples/spring-cloud-gcp-integration-pubsub-json-sample">Pub/Sub JSON Payload Sample App</link> as a reference for using this functionality.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_subscribing_to_a_subscription">
|
||
<title>Subscribing to a subscription</title>
|
||
<simpara>Google Cloud Pub/Sub allows many subscriptions to be associated to the same topic.
|
||
<literal>PubSubTemplate</literal> allows you to listen to subscriptions via the <literal>subscribe()</literal> method.
|
||
It relies on a <literal>SubscriberFactory</literal> object, whose only task is to generate Google Cloud Pub/Sub
|
||
<literal>Subscriber</literal> objects.
|
||
When listening to a subscription, messages will be pulled from Google Cloud Pub/Sub
|
||
asynchronously, at a certain interval.</simpara>
|
||
<simpara>The Spring Boot starter for Google Cloud Pub/Sub auto-configures a <literal>SubscriberFactory</literal>.</simpara>
|
||
<simpara>If Pub/Sub message payload conversion is desired, you can use the <literal>subscribeAndConvert()</literal> method, which will use the converter configured in the template.</simpara>
|
||
</section>
|
||
<section xml:id="_pulling_messages_from_a_subscription">
|
||
<title>Pulling messages from a subscription</title>
|
||
<simpara>Google Cloud Pub/Sub supports synchronous pulling of messages from a subscription.
|
||
This is different from subscribing to a subscription, in the sense that subscribing is an asynchronous task which polls the subscription on a set interval.</simpara>
|
||
<simpara>The <literal>pullNext()</literal> method allows for a single message to be pulled and automatically acknowledged from a subscription.
|
||
The <literal>pull()</literal> method pulls a number of messages from a subscription, allowing for the retry settings to be configured.
|
||
Any messages received by <literal>pull()</literal> are not automatically acknowledged.
|
||
Instead, since they are of the kind <literal>AcknowledgeablePubsubMessage</literal>, you can acknowledge them by calling the <literal>ack()</literal> method, or negatively acknowledge them by calling the <literal>nack()</literal> method.
|
||
The <literal>pullAndAck()</literal> method does the same as the <literal>pull()</literal> method and, additionally, acknowledges all received messages.</simpara>
|
||
<simpara>The <literal>pullAndConvert()</literal> method does the same as the <literal>pull()</literal> method and, additionally, converts the Pub/Sub binary payload to an object of the desired type, using the converter configured in the template.</simpara>
|
||
<simpara>To acknowledge multiple messages received from <literal>pull()</literal> or <literal>pullAndConvert()</literal> at once, you can use the <literal>PubSubTemplate.ack()</literal> method.
|
||
You can also use the <literal>PubSubTemplate.nack()</literal> for negatively acknowledging messages.</simpara>
|
||
<simpara>Using these methods for acknowledging messages in batches is more efficient than acknowledging messages individually, but they <emphasis role="strong">require</emphasis> the collection of messages to be from the same project.</simpara>
|
||
<simpara>All <literal>ack()</literal>, <literal>nack()</literal>, and <literal>modifyAckDeadline()</literal> methods on messages as well as <literal>PubSubSubscriberTemplate</literal> are implemented asynchronously, returning a <literal>ListenableFuture<Void></literal> to be able to process the asynchronous execution.</simpara>
|
||
<simpara><literal>PubSubTemplate</literal> uses a special subscriber generated by its <literal>SubscriberFactory</literal> to synchronously pull messages.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_pubsub_management">
|
||
<title>Pub/Sub management</title>
|
||
<simpara><literal>PubSubAdmin</literal> is the abstraction provided by Spring Cloud GCP to manage Google Cloud Pub/Sub resources.
|
||
It allows for the creation, deletion and listing of topics and subscriptions.</simpara>
|
||
<simpara><literal>PubSubAdmin</literal> depends on <literal>GcpProjectIdProvider</literal> and either a <literal>CredentialsProvider</literal> or a <literal>TopicAdminClient</literal> and a <literal>SubscriptionAdminClient</literal>.
|
||
If given a <literal>CredentialsProvider</literal>, it creates a <literal>TopicAdminClient</literal> and a <literal>SubscriptionAdminClient</literal> with the Google Cloud Java Library for Pub/Sub default settings.
|
||
The Spring Boot starter for GCP Pub/Sub auto-configures a <literal>PubSubAdmin</literal> object using the <literal>GcpProjectIdProvider</literal> and the <literal>CredentialsProvider</literal> auto-configured by the Spring Boot GCP Core starter.</simpara>
|
||
<section xml:id="_creating_a_topic">
|
||
<title>Creating a topic</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to create topics:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public Topic createTopic(String topicName)</programlisting>
|
||
<simpara>Here is an example of how to create a Google Cloud Pub/Sub topic:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void newTopic() {
|
||
pubSubAdmin.createTopic("topicName");
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_deleting_a_topic">
|
||
<title>Deleting a topic</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to delete topics:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void deleteTopic(String topicName)</programlisting>
|
||
<simpara>Here is an example of how to delete a Google Cloud Pub/Sub topic:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void deleteTopic() {
|
||
pubSubAdmin.deleteTopic("topicName");
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_listing_topics">
|
||
<title>Listing topics</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to list topics:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public List<Topic> listTopics</programlisting>
|
||
<simpara>Here is an example of how to list every Google Cloud Pub/Sub topic name in a project:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public List<String> listTopics() {
|
||
return pubSubAdmin
|
||
.listTopics()
|
||
.stream()
|
||
.map(Topic::getNameAsTopicName)
|
||
.map(TopicName::getTopic)
|
||
.collect(Collectors.toList());
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_creating_a_subscription">
|
||
<title>Creating a subscription</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to create subscriptions to existing topics:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline, String pushEndpoint)</programlisting>
|
||
<simpara>Here is an example of how to create a Google Cloud Pub/Sub subscription:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void newSubscription() {
|
||
pubSubAdmin.createSubscription("subscriptionName", "topicName", 10, “http://my.endpoint/push”);
|
||
}</programlisting>
|
||
<simpara>Alternative methods with default settings are provided for ease of use.
|
||
The default value for <literal>ackDeadline</literal> is 10 seconds.
|
||
If <literal>pushEndpoint</literal> isn’t specified, the subscription uses message pulling, instead.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public Subscription createSubscription(String subscriptionName, String topicName)</programlisting>
|
||
<programlisting language="java" linenumbering="unnumbered">public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline)</programlisting>
|
||
<programlisting language="java" linenumbering="unnumbered">public Subscription createSubscription(String subscriptionName, String topicName, String pushEndpoint)</programlisting>
|
||
</section>
|
||
<section xml:id="_deleting_a_subscription">
|
||
<title>Deleting a subscription</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to delete subscriptions:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void deleteSubscription(String subscriptionName)</programlisting>
|
||
<simpara>Here is an example of how to delete a Google Cloud Pub/Sub subscription:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public void deleteSubscription() {
|
||
pubSubAdmin.deleteSubscription("subscriptionName");
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_listing_subscriptions">
|
||
<title>Listing subscriptions</title>
|
||
<simpara><literal>PubSubAdmin</literal> implements a method to list subscriptions:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public List<Subscription> listSubscriptions()</programlisting>
|
||
<simpara>Here is an example of how to list every subscription name in a project:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public List<String> listSubscriptions() {
|
||
return pubSubAdmin
|
||
.listSubscriptions()
|
||
.stream()
|
||
.map(Subscription::getNameAsSubscriptionName)
|
||
.map(SubscriptionName::getSubscription)
|
||
.collect(Collectors.toList());
|
||
}</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="pubsub-configuration">
|
||
<title>Configuration</title>
|
||
<simpara>The Spring Boot starter for Google Cloud Pub/Sub provides the following configuration options:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables or disables Pub/Sub auto-configuration</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.subscriber.executor-threads</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Number of threads used by <literal>Subscriber</literal> instances created by <literal>SubscriberFactory</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>4</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.publisher.executor-threads</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Number of threads used by <literal>Publisher</literal> instances created by <literal>PublisherFactory</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>4</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.project-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>GCP project ID where the Google Cloud Pub/Sub API is hosted, if different from the one in the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>OAuth2 credentials for authenticating with the
|
||
Google Cloud Pub/Sub API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded contents of OAuth2 account private key for authenticating with the
|
||
Google Cloud Pub/Sub API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://developers.google.com/identity/protocols/googlescopes">OAuth2 scope</link> for Spring Cloud GCP
|
||
Pub/Sub credentials</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/pubsub">https://www.googleapis.com/auth/pubsub</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.subscriber.parallel-pull-count</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The number of pull workers</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The available number of processors</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The maximum period a message ack deadline will be extended, in seconds</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.subscriber.pull-endpoint</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The endpoint for synchronous pulling messages</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>pubsub.googleapis.com:443</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.total-timeout-seconds</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely.
|
||
The higher the total timeout, the more retries can be attempted.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-retry-delay-second</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>InitialRetryDelay controls the delay before the first retry.
|
||
Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.retry-delay-multiplier</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>RetryDelayMultiplier controls the change in retry delay.
|
||
The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>1</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-retry-delay-seconds</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier
|
||
can’t increase the retry delay higher than this amount.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-attempts</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>MaxAttempts defines the maximum number of attempts to perform.
|
||
If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.jittered</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Jitter determines if the delay time should be randomized.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>true</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-rpc-timeout-seconds</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>InitialRpcTimeout controls the timeout for the initial RPC.
|
||
Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.rpc-timeout-multiplier</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>RpcTimeoutMultiplier controls the change in RPC timeout.
|
||
The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>1</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-rpc-timeout-seconds</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier
|
||
can’t increase the RPC timeout higher than this amount.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-element-count</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum number of outstanding elements to keep in memory before enforcing flow control.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>unlimited</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-request-bytes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum number of outstanding bytes to keep in memory before enforcing flow control.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>unlimited</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.limit-exceeded-behavior</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The behavior when the specified limits are exceeded.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Block</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The element count threshold to use for batching.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>unset (threshold does not apply)</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The request byte threshold to use for batching.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>unset (threshold does not apply)</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The delay threshold to use for batching.
|
||
After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>unset (threshold does not apply)</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.pubsub.publisher.batching.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables batching.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>false</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_sample">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-pubsub-sample">sample application</link> is available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_resources">
|
||
<title>Spring Resources</title>
|
||
<simpara><link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/html/resources.html">Spring Resources</link> are an abstraction for a number of low-level resources, such as file system files, classpath files, servlet context-relative files, etc.
|
||
Spring Cloud GCP adds a new resource type: a Google Cloud Storage (GCS) object.</simpara>
|
||
<simpara>A Spring Boot starter is provided to auto-configure the various Storage components.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-storage</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
|
||
}</screen>
|
||
<simpara>This starter is also available from <link xl:href="https://start.spring.io/">Spring Initializr</link> through the <literal>GCP Storage</literal> entry.</simpara>
|
||
<section xml:id="_google_cloud_storage">
|
||
<title>Google Cloud Storage</title>
|
||
<simpara>The Spring Resource Abstraction for Google Cloud Storage allows GCS objects to be accessed by their GCS URL using the <literal>@Value</literal> annotation:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
|
||
private Resource gcsResource;</programlisting>
|
||
<simpara>…​or the Spring application context</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">SpringApplication.run(...).getResource("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]");</programlisting>
|
||
<simpara>This creates a <literal>Resource</literal> object that can be used to read the object, among <link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/html/resources.html#resources-resource">other possible operations</link>.</simpara>
|
||
<simpara>It is also possible to write to a <literal>Resource</literal>, although a <literal>WriteableResource</literal> is required.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
|
||
private Resource gcsResource;
|
||
...
|
||
try (OutputStream os = ((WritableResource) gcsResource).getOutputStream()) {
|
||
os.write("foo".getBytes());
|
||
}</programlisting>
|
||
<simpara>To work with the <literal>Resource</literal> as a Google Cloud Storage resource, cast it to <literal>GoogleStorageResource</literal>.</simpara>
|
||
<simpara>If the resource path refers to an object on Google Cloud Storage (as opposed to a bucket), then the <literal>getBlob</literal> method can be called to obtain a <link xl:href="https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java"><literal>Blob</literal></link>.
|
||
This type represents a GCS file, which has associated <link xl:href="https://cloud.google.com/storage/docs/gsutil/addlhelp/WorkingWithObjectMetadata">metadata</link>, such as content-type, that can be set.
|
||
The <literal>createSignedUrl</literal> method can also be used to obtain <link xl:href="https://cloud.google.com/storage/docs/access-control/signed-urls">signed URLs</link> for GCS objects.
|
||
However, creating signed URLs requires that the resource was created using service account credentials.</simpara>
|
||
<simpara>The Spring Boot Starter for Google Cloud Storage auto-configures the <literal>Storage</literal> bean required by the <literal>spring-cloud-gcp-storage</literal> module, based on the <literal>CredentialsProvider</literal> provided by the Spring Boot GCP starter.</simpara>
|
||
<section xml:id="_setting_the_content_type">
|
||
<title>Setting the Content Type</title>
|
||
<simpara>You can set the content-type of Google Cloud Storage files from their corresponding <literal>Resource</literal> objects:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">((GoogleStorageResource)gcsResource).getBlob().toBuilder().setContentType("text/html").build().update();</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_configuration">
|
||
<title>Configuration</title>
|
||
<simpara>The Spring Boot Starter for Google Cloud Storage provides the following configuration options:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.storage.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables the GCP storage APIs.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.storage.auto-create-files</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Creates files and buckets on Google Cloud Storage when writes are made to non-existent files</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.storage.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>OAuth2 credentials for authenticating with the Google Cloud Storage API, if different from the ones in the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.storage.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded contents of OAuth2 account private key for authenticating with the Google Cloud Storage API, if different from the ones in the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.storage.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://developers.google.com/identity/protocols/googlescopes">OAuth2 scope</link> for Spring Cloud GCP Storage credentials</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/devstorage.read_write">https://www.googleapis.com/auth/devstorage.read_write</link></simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_sample_2">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-storage-resource-sample">sample application</link> and a <link xl:href="https://codelabs.developers.google.com/codelabs/spring-cloud-gcp-gcs/index.html">codelab</link> are available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_jdbc">
|
||
<title>Spring JDBC</title>
|
||
<simpara>Spring Cloud GCP adds integrations with
|
||
<link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html">Spring JDBC</link> so you can run your MySQL or PostgreSQL databases in Google Cloud SQL using Spring JDBC, or other libraries that depend on it like Spring Data JPA.</simpara>
|
||
<simpara>The Cloud SQL support is provided by Spring Cloud GCP in the form of two Spring Boot starters, one for MySQL and another one for PostgreSQL.
|
||
The role of the starters is to read configuration from properties and assume default settings so that user experience connecting to MySQL and PostgreSQL is as simple as possible.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-sql-postgresql</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-mysql'
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-postgresql'
|
||
}</screen>
|
||
<section xml:id="_prerequisites">
|
||
<title>Prerequisites</title>
|
||
<simpara>In order to use the Spring Boot Starters for Google Cloud SQL, the Google Cloud SQL API must be enabled in your GCP project.</simpara>
|
||
<simpara>To do that, go to the <link xl:href="https://console.cloud.google.com/apis/library">API library page</link> of the Google Cloud Console, search for "Cloud SQL API", click the first result and enable the API.</simpara>
|
||
<note>
|
||
<simpara>There are several similar "Cloud SQL" results.
|
||
You must access the "Google Cloud SQL API" one and enable the API from there.</simpara>
|
||
</note>
|
||
</section>
|
||
<section xml:id="_spring_boot_starter_for_google_cloud_sql">
|
||
<title>Spring Boot Starter for Google Cloud SQL</title>
|
||
<simpara>The Spring Boot Starters for Google Cloud SQL provide an auto-configured <link xl:href="https://docs.oracle.com/javase/7/docs/api/javax/sql/DataSource.html"><literal>DataSource</literal></link> object.
|
||
Coupled with Spring JDBC, it provides a
|
||
<link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-JdbcTemplate"><literal>JdbcTemplate</literal></link> object bean that allows for operations such as querying and modifying a database.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public List<Map<String, Object>> listUsers() {
|
||
return jdbcTemplate.queryForList("SELECT * FROM user;");
|
||
}</programlisting>
|
||
<simpara>You can rely on
|
||
<link xl:href="https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-connect-to-production-database">Spring Boot data source auto-configuration</link> to configure a <literal>DataSource</literal> bean.
|
||
In other words, properties like the SQL username, <literal>spring.datasource.username</literal>, and password, <literal>spring.datasource.password</literal> can be used.
|
||
There is also some configuration specific to Google Cloud SQL:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Property name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Unused if specified property(ies)</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.sql.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables or disables Cloud SQL auto configuration</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.sql.database-name</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Name of the database to connect to.</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
<entry align="left" valign="top"><simpara><literal>spring.datasource.url</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.sql.instance-connection-name</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>A string containing a Google Cloud SQL instance’s project ID, region and name, each separated by a colon.
|
||
For example, <literal>my-project-id:my-region:my-instance-name</literal>.</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
<entry align="left" valign="top"><simpara><literal>spring.datasource.url</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.sql.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>File system path to the Google OAuth2 credentials private key file.
|
||
Used to authenticate and authorize new connections to a Google Cloud SQL instance.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default credentials provided by the Spring GCP Boot starter</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.sql.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded contents of OAuth2 account private key in JSON format.
|
||
Used to authenticate and authorize new connections to a Google Cloud SQL instance.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default credentials provided by the Spring GCP Boot starter</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<section xml:id="_datasource_creation_flow">
|
||
<title><literal>DataSource</literal> creation flow</title>
|
||
<simpara>Based on the previous properties, the Spring Boot starter for Google Cloud SQL creates a <literal>CloudSqlJdbcInfoProvider</literal> object which is used to obtain an instance’s JDBC URL and driver class name.
|
||
If you provide your own <literal>CloudSqlJdbcInfoProvider</literal> bean, it is used instead and the properties related to building the JDBC URL or driver class are ignored.</simpara>
|
||
<simpara>The <literal>DataSourceProperties</literal> object provided by Spring Boot Autoconfigure is mutated in order to use the JDBC URL and driver class names provided by <literal>CloudSqlJdbcInfoProvider</literal>, unless those values were provided in the properties.
|
||
It is in the <literal>DataSourceProperties</literal> mutation step that the credentials factory is registered in a system property to be <literal>SqlCredentialFactory</literal>.</simpara>
|
||
<simpara><literal>DataSource</literal> creation is delegated to
|
||
<link xl:href="https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html">Spring Boot</link>.
|
||
You can select the type of connection pool (e.g., Tomcat, HikariCP, etc.) by <link xl:href="https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-connect-to-production-database">adding their dependency to the classpath</link>.</simpara>
|
||
<simpara>Using the created <literal>DataSource</literal> in conjunction with Spring JDBC provides you with a fully configured and operational <literal>JdbcTemplate</literal> object that you can use to interact with your SQL database.
|
||
You can connect to your database with as little as a database and instance names.</simpara>
|
||
</section>
|
||
<section xml:id="_troubleshooting_tips">
|
||
<title>Troubleshooting tips</title>
|
||
<section xml:id="connection-issues">
|
||
<title>Connection issues</title>
|
||
<simpara>If you’re not able to connect to a database and see an endless loop of <literal>Connecting to Cloud SQL instance […​] on IP […​]</literal>, it’s likely that exceptions are being thrown and logged at a level lower than your logger’s level.
|
||
This may be the case with HikariCP, if your logger is set to INFO or higher level.</simpara>
|
||
<simpara>To see what’s going on in the background, you should add a <literal>logback.xml</literal> file to your application resources folder, that looks like this:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><?xml version="1.0" encoding="UTF-8"?>
|
||
<configuration>
|
||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||
<logger name="com.zaxxer.hikari.pool" level="DEBUG"/>
|
||
</configuration></programlisting>
|
||
</section>
|
||
<section xml:id="_errors_like_c_g_cloud_sql_core_sslsocketfactory_re_throwing_cached_exception_due_to_attempt_to_refresh_instance_information_too_soon_after_error">
|
||
<title>Errors like <literal>c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error</literal></title>
|
||
<simpara>If you see a lot of errors like this in a loop and can’t connect to your database, this is usually a symptom that something isn’t right with the permissions of your credentials or the Google Cloud SQL API is not enabled.
|
||
Verify that the Google Cloud SQL API is enabled in the Cloud Console and that your service account has the <link xl:href="https://cloud.google.com/sql/docs/mysql/project-access-control#roles">necessary IAM roles</link>.</simpara>
|
||
<simpara>To find out what’s causing the issue, you can enable DEBUG logging level as mentioned <link xl:href="#connection-issues">above</link>.</simpara>
|
||
</section>
|
||
<section xml:id="_postgresql_java_net_socketexception_already_connected_issue">
|
||
<title>PostgreSQL: <literal>java.net.SocketException: already connected</literal> issue</title>
|
||
<simpara>We found this exception to be common if your Maven project’s parent is <literal>spring-boot</literal> version <literal>1.5.x</literal>, or in any other circumstance that would cause the version of the <literal>org.postgresql:postgresql</literal> dependency to be an older one (e.g., <literal>9.4.1212.jre7</literal>).</simpara>
|
||
<simpara>To fix this, re-declare the dependency in its correct version.
|
||
For example, in Maven:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.postgresql</groupId>
|
||
<artifactId>postgresql</artifactId>
|
||
<version>42.1.1</version>
|
||
</dependency></programlisting>
|
||
</section>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_samples">
|
||
<title>Samples</title>
|
||
<simpara>Available sample applications and codelabs:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-sql-mysql-sample">Spring Cloud GCP MySQL</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-sql-postgres-sample">Spring Cloud GCP PostgreSQL</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-jpa-sample">Spring Data JPA with Spring Cloud GCP SQL</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Codelab: <link xl:href="https://codelabs.developers.google.com/codelabs/cloud-spring-petclinic-cloudsql/index.html">Spring Pet Clinic using Cloud SQL</link></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_integration">
|
||
<title>Spring Integration</title>
|
||
<simpara>Spring Cloud GCP provides Spring Integration adapters that allow your applications to use Enterprise Integration Patterns backed up by Google Cloud Platform services.</simpara>
|
||
<section xml:id="_channel_adapters_for_cloud_pubsub">
|
||
<title>Channel Adapters for Cloud Pub/Sub</title>
|
||
<simpara>The channel adapters for Google Cloud Pub/Sub connect your Spring <link xl:href="https://docs.spring.io/spring-integration/reference/html/messaging-channels-section.html#channel"><literal>MessageChannels</literal></link> to Google Cloud Pub/Sub topics and subscriptions.
|
||
This enables messaging between different processes, applications or micro-services backed up by Google Cloud Pub/Sub.</simpara>
|
||
<simpara>The Spring Integration Channel Adapters for Google Cloud Pub/Sub are included in the <literal>spring-cloud-gcp-pubsub</literal> module.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-pubsub</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.integration</groupId>
|
||
<artifactId>spring-integration-core</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub'
|
||
compile group: 'org.springframework.integration', name: 'spring-integration-core'
|
||
}</screen>
|
||
<section xml:id="_inbound_channel_adapter">
|
||
<title>Inbound channel adapter</title>
|
||
<simpara><literal>PubSubInboundChannelAdapter</literal> is the inbound channel adapter for GCP Pub/Sub that listens to a GCP Pub/Sub subscription for new messages.
|
||
It converts new messages to an internal Spring <link xl:href="https://docs.spring.io/spring-integration/reference/html/messaging-construction-chapter.html#message"><literal>Message</literal></link> and then sends it to the bound output channel.</simpara>
|
||
<simpara>Google Pub/Sub treats message payloads as byte arrays.
|
||
So, by default, the inbound channel adapter will construct the Spring <literal>Message</literal> with <literal>byte[]</literal> as the payload.
|
||
However, you can change the desired payload type by setting the <literal>payloadType</literal> property of the <literal>PubSubInboundChannelAdapter</literal>.
|
||
The <literal>PubSubInboundChannelAdapter</literal> delegates the conversion to the desired payload type to the <literal>PubSubMessageConverter</literal> configured in the <literal>PubSubTemplate</literal>.</simpara>
|
||
<simpara>To use the inbound channel adapter, a <literal>PubSubInboundChannelAdapter</literal> must be provided and configured on the user application side.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
public MessageChannel pubsubInputChannel() {
|
||
return new PublishSubscribeChannel();
|
||
}
|
||
|
||
@Bean
|
||
public PubSubInboundChannelAdapter messageChannelAdapter(
|
||
@Qualifier("pubsubInputChannel") MessageChannel inputChannel,
|
||
SubscriberFactory subscriberFactory) {
|
||
PubSubInboundChannelAdapter adapter =
|
||
new PubSubInboundChannelAdapter(subscriberFactory, "subscriptionName");
|
||
adapter.setOutputChannel(inputChannel);
|
||
adapter.setAckMode(AckMode.MANUAL);
|
||
|
||
return adapter;
|
||
}</programlisting>
|
||
<simpara>In the example, we first specify the <literal>MessageChannel</literal> where the adapter is going to write incoming messages to.
|
||
The <literal>MessageChannel</literal> implementation isn’t important here.
|
||
Depending on your use case, you might want to use a <literal>MessageChannel</literal> other than <literal>PublishSubscribeChannel</literal>.</simpara>
|
||
<simpara>Then, we declare a <literal>PubSubInboundChannelAdapter</literal> bean.
|
||
It requires the channel we just created and a <literal>SubscriberFactory</literal>, which creates <literal>Subscriber</literal> objects from the Google Cloud Java Client for Pub/Sub.
|
||
The Spring Boot starter for GCP Pub/Sub provides a configured <literal>SubscriberFactory</literal>.</simpara>
|
||
<simpara>The <literal>PubSubInboundChannelAdapter</literal> supports three acknowledgement modes, with <literal>AckMode.AUTO</literal> being the default value;</simpara>
|
||
<simpara>Automatic acking (<literal>AckMode.AUTO</literal>)</simpara>
|
||
<simpara>A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown.
|
||
If a <literal>RuntimeException</literal> is thrown while the message is processed, then the message is nacked.</simpara>
|
||
<simpara>Automatic acking OK (<literal>AckMode.AUTO_ACK</literal>)</simpara>
|
||
<simpara>A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown.
|
||
If a <literal>RuntimeException</literal> is thrown while the message is processed, then the message is neither acked / nor nacked.</simpara>
|
||
<simpara>This is useful when using the subscription’s ack deadline timeout as a retry delivery backoff mechanism.</simpara>
|
||
<simpara>Manually acking (<literal>AckMode.MANUAL</literal>)</simpara>
|
||
<simpara>The adapter attaches a <literal>BasicAcknowledgeablePubsubMessage</literal> object to the <literal>Message</literal> headers.
|
||
Users can extract the <literal>BasicAcknowledgeablePubsubMessage</literal> using the <literal>GcpPubSubHeaders.ORIGINAL_MESSAGE</literal> key and use it to (n)ack a message.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
@ServiceActivator(inputChannel = "pubsubInputChannel")
|
||
public MessageHandler messageReceiver() {
|
||
return message -> {
|
||
LOGGER.info("Message arrived! Payload: " + new String((byte[]) message.getPayload()));
|
||
BasicAcknowledgeablePubsubMessage originalMessage =
|
||
message.getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class);
|
||
originalMessage.ack();
|
||
};
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_outbound_channel_adapter">
|
||
<title>Outbound channel adapter</title>
|
||
<simpara><literal>PubSubMessageHandler</literal> is the outbound channel adapter for GCP Pub/Sub that listens for new messages on a Spring <literal>MessageChannel</literal>.
|
||
It uses <literal>PubSubTemplate</literal> to post them to a GCP Pub/Sub topic.</simpara>
|
||
<simpara>To construct a Pub/Sub representation of the message, the outbound channel adapter needs to convert the Spring <literal>Message</literal> payload to a byte array representation expected by Pub/Sub.
|
||
It delegates this conversion to the <literal>PubSubTemplate</literal>.
|
||
To customize the conversion, you can specify a <literal>PubSubMessageConverter</literal> in the <literal>PubSubTemplate</literal> that should convert the <literal>Object</literal> payload and headers of the Spring <literal>Message</literal> to a <literal>PubsubMessage</literal>.</simpara>
|
||
<simpara>To use the outbound channel adapter, a <literal>PubSubMessageHandler</literal> bean must be provided and configured on the user application side.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
@ServiceActivator(inputChannel = "pubsubOutputChannel")
|
||
public MessageHandler messageSender(PubSubTemplate pubsubTemplate) {
|
||
return new PubSubMessageHandler(pubsubTemplate, "topicName");
|
||
}</programlisting>
|
||
<simpara>The provided <literal>PubSubTemplate</literal> contains all the necessary configuration to publish messages to a GCP Pub/Sub topic.</simpara>
|
||
<simpara><literal>PubSubMessageHandler</literal> publishes messages asynchronously by default.
|
||
A publish timeout can be configured for synchronous publishing.
|
||
If none is provided, the adapter waits indefinitely for a response.</simpara>
|
||
<simpara>It is possible to set user-defined callbacks for the <literal>publish()</literal> call in <literal>PubSubMessageHandler</literal> through the <literal>setPublishFutureCallback()</literal> method.
|
||
These are useful to process the message ID, in case of success, or the error if any was thrown.</simpara>
|
||
<simpara>To override the default destination you can use the <literal>GcpPubSubHeaders.DESTINATION</literal> header.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
private MessageChannel pubsubOutputChannel;
|
||
|
||
public void handleMessage(Message<?> msg) throws MessagingException {
|
||
final Message<?> message = MessageBuilder
|
||
.withPayload(msg.getPayload())
|
||
.setHeader(GcpPubSubHeaders.TOPIC, "customTopic").build();
|
||
pubsubOutputChannel.send(message);
|
||
}</programlisting>
|
||
<simpara>It is also possible to set an SpEL expression for the topic with the <literal>setTopicExpression()</literal> or <literal>setTopicExpressionString()</literal> methods.</simpara>
|
||
</section>
|
||
<section xml:id="_header_mapping">
|
||
<title>Header mapping</title>
|
||
<simpara>These channel adapters contain header mappers that allow you to map, or filter out, headers from Spring to Google Cloud Pub/Sub messages, and vice-versa.
|
||
By default, the inbound channel adapter maps every header on the Google Cloud Pub/Sub messages to the Spring messages produced by the adapter.
|
||
The outbound channel adapter maps every header from Spring messages into Google Cloud Pub/Sub ones, except the ones added by Spring, like headers with key <literal>"id"</literal>, <literal>"timestamp"</literal> and <literal>"gcp_pubsub_acknowledgement"</literal>.
|
||
In the process, the outbound mapper also converts the value of the headers into string.</simpara>
|
||
<simpara>Each adapter declares a <literal>setHeaderMapper()</literal> method to let you further customize which headers you want to map from Spring to Google Cloud Pub/Sub, and vice-versa.</simpara>
|
||
<simpara>For example, to filter out headers <literal>"foo"</literal>, <literal>"bar"</literal> and all headers starting with the prefix "prefix_", you can use <literal>setHeaderMapper()</literal> along with the <literal>PubSubHeaderMapper</literal> implementation provided by this module.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">PubSubMessageHandler adapter = ...
|
||
...
|
||
PubSubHeaderMapper headerMapper = new PubSubHeaderMapper();
|
||
headerMapper.setOutboundHeaderPatterns("!foo", "!bar", "!prefix_*", "*");
|
||
adapter.setHeaderMapper(headerMapper);</programlisting>
|
||
<note>
|
||
<simpara>The order in which the patterns are declared in <literal>PubSubHeaderMapper.setOutboundHeaderPatterns()</literal> and <literal>PubSubHeaderMapper.setInboundHeaderPatterns()</literal> matters.
|
||
The first patterns have precedence over the following ones.</simpara>
|
||
</note>
|
||
<simpara>In the previous example, the <literal>"*"</literal> pattern means every header is mapped.
|
||
However, because it comes last in the list, <link xl:href="https://docs.spring.io/spring-integration/api/org/springframework/integration/util/PatternMatchUtils.html#smartMatch-java.lang.String-java.lang.String…​-">the previous patterns take precedence</link>.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_sample_3">
|
||
<title>Sample</title>
|
||
<simpara>Available examples:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-integration-pubsub-sample">sender and receiver sample application</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-integration-pubsub-json-sample">JSON payloads sample application</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://codelabs.developers.google.com/codelabs/cloud-spring-cloud-gcp-pubsub-integration/index.html">codelab</link></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
<section xml:id="_channel_adapters_for_google_cloud_storage">
|
||
<title>Channel Adapters for Google Cloud Storage</title>
|
||
<simpara>The channel adapters for Google Cloud Storage allow you to read and write files to Google Cloud Storage through <literal>MessageChannels</literal>.</simpara>
|
||
<simpara>Spring Cloud GCP provides two inbound adapters, <literal>GcsInboundFileSynchronizingMessageSource</literal> and <literal>GcsStreamingMessageSource</literal>, and one outbound adapter, <literal>GcsMessageHandler</literal>.</simpara>
|
||
<simpara>The Spring Integration Channel Adapters for Google Cloud Storage are included in the <literal>spring-cloud-gcp-storage</literal> module.</simpara>
|
||
<simpara>To use the Storage portion of Spring Integration for Spring Cloud GCP, you must also provide the <literal>spring-integration-file</literal> dependency, since it isn’t pulled transitively.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-storage</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.integration</groupId>
|
||
<artifactId>spring-integration-file</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
|
||
compile group: 'org.springframework.integration', name: 'spring-integration-file'
|
||
}</screen>
|
||
<section xml:id="_inbound_channel_adapter_2">
|
||
<title>Inbound channel adapter</title>
|
||
<simpara>The Google Cloud Storage inbound channel adapter polls a Google Cloud Storage bucket for new files and sends each of them in a <literal>Message</literal> payload to the <literal>MessageChannel</literal> specified in the <literal>@InboundChannelAdapter</literal> annotation.
|
||
The files are temporarily stored in a folder in the local file system.</simpara>
|
||
<simpara>Here is an example of how to configure a Google Cloud Storage inbound channel adapter.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
@InboundChannelAdapter(channel = "new-file-channel", poller = @Poller(fixedDelay = "5000"))
|
||
public MessageSource<File> synchronizerAdapter(Storage gcs) {
|
||
GcsInboundFileSynchronizer synchronizer = new GcsInboundFileSynchronizer(gcs);
|
||
synchronizer.setRemoteDirectory("your-gcs-bucket");
|
||
|
||
GcsInboundFileSynchronizingMessageSource synchAdapter =
|
||
new GcsInboundFileSynchronizingMessageSource(synchronizer);
|
||
synchAdapter.setLocalDirectory(new File("local-directory"));
|
||
|
||
return synchAdapter;
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_inbound_streaming_channel_adapter">
|
||
<title>Inbound streaming channel adapter</title>
|
||
<simpara>The inbound streaming channel adapter is similar to the normal inbound channel adapter, except it does not require files to be stored in the file system.</simpara>
|
||
<simpara>Here is an example of how to configure a Google Cloud Storage inbound streaming channel adapter.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
@InboundChannelAdapter(channel = "streaming-channel", poller = @Poller(fixedDelay = "5000"))
|
||
public MessageSource<InputStream> streamingAdapter(Storage gcs) {
|
||
GcsStreamingMessageSource adapter =
|
||
new GcsStreamingMessageSource(new GcsRemoteFileTemplate(new GcsSessionFactory(gcs)));
|
||
adapter.setRemoteDirectory("your-gcs-bucket");
|
||
return adapter;
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_outbound_channel_adapter_2">
|
||
<title>Outbound channel adapter</title>
|
||
<simpara>The outbound channel adapter allows files to be written to Google Cloud Storage.
|
||
When it receives a <literal>Message</literal> containing a payload of type <literal>File</literal>, it writes that file to the Google Cloud Storage bucket specified in the adapter.</simpara>
|
||
<simpara>Here is an example of how to configure a Google Cloud Storage outbound channel adapter.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Bean
|
||
@ServiceActivator(inputChannel = "writeFiles")
|
||
public MessageHandler outboundChannelAdapter(Storage gcs) {
|
||
GcsMessageHandler outboundChannelAdapter = new GcsMessageHandler(new GcsSessionFactory(gcs));
|
||
outboundChannelAdapter.setRemoteDirectoryExpression(new ValueExpression<>("your-gcs-bucket"));
|
||
|
||
return outboundChannelAdapter;
|
||
}</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_sample_4">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-integration-storage-sample">sample application</link> is available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_cloud_stream">
|
||
<title>Spring Cloud Stream</title>
|
||
<simpara>Spring Cloud GCP provides a <link xl:href="https://cloud.spring.io/spring-cloud-stream/">Spring Cloud Stream</link> binder to Google Cloud Pub/Sub.</simpara>
|
||
<simpara>The provided binder relies on the <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-pubsub/src/main/java/org/springframework/cloud/gcp/pubsub/integration">Spring Integration Channel Adapters for Google Cloud Pub/Sub</link>.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-pubsub-stream-binder</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub-stream-binder'
|
||
}</screen>
|
||
<section xml:id="_overview">
|
||
<title>Overview</title>
|
||
<simpara>This binder binds producers to Google Cloud Pub/Sub topics and consumers to subscriptions.</simpara>
|
||
<note>
|
||
<simpara>Partitioning is currently not supported by this binder.</simpara>
|
||
</note>
|
||
</section>
|
||
<section xml:id="_configuration_2">
|
||
<title>Configuration</title>
|
||
<simpara>You can configure the Spring Cloud Stream Binder for Google Cloud Pub/Sub to automatically generate the underlying resources, like the Google Cloud Pub/Sub topics and subscriptions for producers and consumers.
|
||
For that, you can use the <literal>spring.cloud.stream.gcp.pubsub.bindings.<channelName>.<consumer|producer>.auto-create-resources</literal> property, which is turned ON by default.</simpara>
|
||
<simpara>Starting with version 1.1, these and other binder properties can be configured globally for all the bindings, e.g. <literal>spring.cloud.stream.gcp.pubsub.default.consumer.auto-create-resources</literal>.</simpara>
|
||
<simpara>If you are using Pub/Sub auto-configuration from the Spring Cloud GCP Pub/Sub Starter, you should refer to the <link linkend="pubsub-configuration">configuration</link> section for other Pub/Sub parameters.</simpara>
|
||
<note>
|
||
<simpara>To use this binder with a <link xl:href="https://cloud.google.com/pubsub/docs/emulator">running emulator</link>, configure its host and port via <literal>spring.cloud.gcp.pubsub.emulator-host</literal>.</simpara>
|
||
</note>
|
||
<section xml:id="_producer_destination_configuration">
|
||
<title>Producer Destination Configuration</title>
|
||
<simpara>If automatic resource creation is turned ON and the topic corresponding to the destination name does not exist, it will be created.</simpara>
|
||
<simpara>For example, for the following configuration, a topic called <literal>myEvents</literal> would be created.</simpara>
|
||
<formalpara>
|
||
<title>application.properties</title>
|
||
<para>
|
||
<screen>spring.cloud.stream.bindings.events.destination=myEvents
|
||
spring.cloud.stream.gcp.pubsub.bindings.events.producer.auto-create-resources=true</screen>
|
||
</para>
|
||
</formalpara>
|
||
</section>
|
||
<section xml:id="_consumer_destination_configuration">
|
||
<title>Consumer Destination Configuration</title>
|
||
<simpara>If automatic resource creation is turned ON and the subscription and/or the topic do not exist for a consumer, a subscription and potentially a topic will be created.
|
||
The topic name will be the same as the destination name, and the subscription name will be the destination name followed by the consumer group name.</simpara>
|
||
<simpara>Regardless of the <literal>auto-create-resources</literal> setting, if the consumer group is not specified, an anonymous one will be created with the name <literal>anonymous.<destinationName>.<randomUUID></literal>.
|
||
Then when the binder shuts down, all Pub/Sub subscriptions created for anonymous consumer groups will be automatically cleaned up.</simpara>
|
||
<simpara>For example, for the following configuration, a topic named <literal>myEvents</literal> and a subscription called <literal>myEvents.counsumerGroup1</literal> would be created.
|
||
If the consumer group is not specified, a subscription called <literal>anonymous.myEvents.a6d83782-c5a3-4861-ac38-e6e2af15a7be</literal> would be created and later cleaned up.</simpara>
|
||
<important>
|
||
<simpara>If you are manually creating Pub/Sub subscriptions for consumers, make sure that they follow the naming convention of <literal><destinationName>.<consumerGroup></literal>.</simpara>
|
||
</important>
|
||
<formalpara>
|
||
<title>application.properties</title>
|
||
<para>
|
||
<screen>spring.cloud.stream.bindings.events.destination=myEvents
|
||
spring.cloud.stream.gcp.pubsub.bindings.events.consumer.auto-create-resources=true
|
||
|
||
# specify consumer group, and avoid anonymous consumer group generation
|
||
spring.cloud.stream.bindings.events.group=consumerGroup1</screen>
|
||
</para>
|
||
</formalpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_sample_5">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-pubsub-binder-sample">sample application</link> is available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_cloud_sleuth">
|
||
<title>Spring Cloud Sleuth</title>
|
||
<simpara><link xl:href="https://cloud.spring.io/spring-cloud-sleuth/">Spring Cloud Sleuth</link> is an instrumentation framework for Spring Boot applications.
|
||
It captures trace information and can forward traces to services like Zipkin for storage and analysis.</simpara>
|
||
<simpara>Google Cloud Platform provides its own managed distributed tracing service called <link xl:href="https://cloud.google.com/trace/">Stackdriver Trace</link>.
|
||
Instead of running and maintaining your own Zipkin instance and storage, you can use Stackdriver Trace to store traces, view trace details, generate latency distributions graphs, and generate performance regression reports.</simpara>
|
||
<simpara>This Spring Cloud GCP starter can forward Spring Cloud Sleuth traces to Stackdriver Trace without an intermediary Zipkin server.</simpara>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-trace</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-trace'
|
||
}</screen>
|
||
<simpara>You must enable Stackdriver Trace API from the Google Cloud Console in order to capture traces.
|
||
Navigate to the <link xl:href="https://console.cloud.google.com/apis/api/cloudtrace.googleapis.com/overview">Stackdriver Trace API</link> for your project and make sure it’s enabled.</simpara>
|
||
<note>
|
||
<simpara>If you are already using a Zipkin server capturing trace information from multiple platform/frameworks, you can also use a <link xl:href="https://cloud.google.com/trace/docs/zipkin">Stackdriver Zipkin proxy</link> to forward those traces to Stackdriver Trace without modifying existing applications.</simpara>
|
||
</note>
|
||
<section xml:id="_tracing">
|
||
<title>Tracing</title>
|
||
<simpara>Spring Cloud Sleuth uses the <link xl:href="https://github.com/openzipkin/brave">Brave tracer</link> to generate traces.
|
||
This integration enables Brave to use the <link xl:href="https://github.com/openzipkin/zipkin-gcp/tree/master/propagation-stackdriver"><literal>StackdriverTracePropagation</literal></link> propagation.</simpara>
|
||
<simpara>A propagation is responsible for extracting trace context from an entity (e.g., an HTTP servlet request) and injecting trace context into an entity.
|
||
A canonical example of the propagation usage is a web server that receives an HTTP request, which triggers other HTTP requests from the server before returning an HTTP response to the original caller.
|
||
In the case of <literal>StackdriverTracePropagation</literal>, first it looks for trace context in the <literal>x-cloud-trace-context</literal> key (e.g., an HTTP request header).
|
||
The value of the <literal>x-cloud-trace-context</literal> key can be formatted in three different ways:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>x-cloud-trace-context: TRACE_ID</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>x-cloud-trace-context: TRACE_ID/SPAN_ID</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>x-cloud-trace-context: TRACE_ID/SPAN_ID;o=TRACE_TRUE</literal></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara><literal>TRACE_ID</literal> is a 32-character hexadecimal value that encodes a 128-bit number.</simpara>
|
||
<simpara><literal>SPAN_ID</literal> is an unsigned long.
|
||
Since Stackdriver Trace doesn’t support span joins, a new span ID is always generated, regardless of the one specified in <literal>x-cloud-trace-context</literal>.</simpara>
|
||
<simpara><literal>TRACE_TRUE</literal> can either be <literal>0</literal> if the entity should be untraced, or <literal>1</literal> if it should be traced.
|
||
This field forces the decision of whether or not to trace the request; if omitted then the decision is deferred to the sampler.</simpara>
|
||
<simpara>If a <literal>x-cloud-trace-context</literal> key isn’t found, <literal>StackdriverTracePropagation</literal> falls back to tracing with the <link xl:href="https://github.com/openzipkin/b3-propagation">X-B3 headers</link>.</simpara>
|
||
</section>
|
||
<section xml:id="_spring_boot_starter_for_stackdriver_trace">
|
||
<title>Spring Boot Starter for Stackdriver Trace</title>
|
||
<simpara>Spring Boot Starter for Stackdriver Trace uses Spring Cloud Sleuth and auto-configures a <link xl:href="https://github.com/openzipkin/zipkin-gcp/blob/master/sender-stackdriver/src/main/java/zipkin2/reporter/stackdriver/StackdriverSender.java">StackdriverSender</link> that sends the Sleuth’s trace information to Stackdriver Trace.</simpara>
|
||
<simpara>All configurations are optional:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Auto-configure Spring Cloud Sleuth to send traces to Stackdriver Trace.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.project-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Overrides the project ID from the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Overrides the credentials location from the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Overrides the credentials encoded key from the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Overrides the credentials scopes from the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.num-executor-threads</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Number of threads used by the Trace executor</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>4</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.authority</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>HTTP/2 authority the channel claims to be connecting to.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.compression</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Name of the compression to use in Trace calls</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.deadline-ms</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Call deadline in milliseconds</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.max-inbound-size</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum size for inbound messages</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.max-outbound-size</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum size for outbound messages</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.trace.wait-for-ready</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">Waits for the channel to be ready</link> in case of a transient failure</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>false</literal></simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<simpara>You can use core Spring Cloud Sleuth properties to control Sleuth’s sampling rate, etc.
|
||
Read <link xl:href="https://cloud.spring.io/spring-cloud-sleuth/">Sleuth documentation</link> for more information on Sleuth configurations.</simpara>
|
||
<simpara>For example, when you are testing to see the traces are going through, you can set the sampling rate to 100%.</simpara>
|
||
<screen>spring.sleuth.sampler.probability=1 # Send 100% of the request traces to Stackdriver.
|
||
spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*) # Ignore some URL paths.</screen>
|
||
<simpara>Spring Cloud GCP Trace does override some Sleuth configurations:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Always uses 128-bit Trace IDs.
|
||
This is required by Stackdriver Trace.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Does not use Span joins.
|
||
Span joins will share the span ID between the client and server Spans.
|
||
Stackdriver requires that every Span ID within a Trace to be unique, so Span joins are not supported.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Uses <literal>StackdriverHttpClientParser</literal> and <literal>StackdriverHttpServerParser</literal> by default to populate Stackdriver related fields.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
<section xml:id="_integration_with_logging">
|
||
<title>Integration with Logging</title>
|
||
<simpara>Integration with Stackdriver Logging is available through the <link xl:href="logging.adoc">Stackdriver Logging Support</link>.
|
||
If the Trace integration is used together with the Logging one, the request logs will be associated to the corresponding traces.
|
||
The trace logs can be viewed by going to the <link xl:href="https://console.cloud.google.com/traces/traces">Google Cloud Console Trace List</link>, selecting a trace and pressing the <literal>Logs → View</literal> link in the <literal>Details</literal> section.</simpara>
|
||
</section>
|
||
<section xml:id="_sample_6">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-trace-sample">sample application</link> and a <link xl:href="https://codelabs.developers.google.com/codelabs/cloud-spring-cloud-gcp-trace/index.html">codelab</link> are available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_stackdriver_logging">
|
||
<title>Stackdriver Logging</title>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-logging</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging'
|
||
}</screen>
|
||
<simpara><link xl:href="https://cloud.google.com/logging/">Stackdriver Logging</link> is the managed logging service provided by Google Cloud Platform.</simpara>
|
||
<simpara>This module provides support for associating a web request trace ID with the corresponding log entries.
|
||
It does so by retrieving the <literal>X-B3-TraceId</literal> value from the <link xl:href="https://logback.qos.ch/manual/mdc.html">Mapped Diagnostic Context (MDC)</link>, which is set by Spring Cloud Sleuth.
|
||
If Spring Cloud Sleuth isn’t used, the configured <literal>TraceIdExtractor</literal> extracts the desired header value and sets it as the log entry’s trace ID.
|
||
This allows grouping of log messages by request, for example, in the <link xl:href="https://console.cloud.google.com/logs/viewer">Google Cloud Console Logs viewer</link>.</simpara>
|
||
<note>
|
||
<simpara>Due to the way logging is set up, the GCP project ID and credentials defined in <literal>application.properties</literal> are ignored.
|
||
Instead, you should set the <literal>GOOGLE_CLOUD_PROJECT</literal> and <literal>GOOGLE_APPLICATION_CREDENTIALS</literal> environment variables to the project ID and credentials private key location, respectively.
|
||
You can do this easily if you’re using the <link xl:href="http://cloud.google.com/sdk">Google Cloud SDK</link>, using the <literal>gcloud config set project [YOUR_PROJECT_ID]</literal> and <literal>gcloud auth application-default login</literal> commands, respectively.</simpara>
|
||
</note>
|
||
<section xml:id="_web_mvc_interceptor">
|
||
<title>Web MVC Interceptor</title>
|
||
<simpara>For use in Web MVC-based applications, <literal>TraceIdLoggingWebMvcInterceptor</literal> is provided that extracts the request trace ID from an HTTP request using a <literal>TraceIdExtractor</literal> and stores it in a thread-local, which can then be used in a logging appender to add the trace ID metadata to log messages.</simpara>
|
||
<warning>
|
||
<simpara>If Spring Cloud GCP Trace is enabled, the logging module disables itself and delegates log correlation to Spring Cloud Sleuth.</simpara>
|
||
</warning>
|
||
<simpara><literal>LoggingWebMvcConfigurer</literal> configuration class is also provided to help register the <literal>TraceIdLoggingWebMvcInterceptor</literal> in Spring MVC applications.</simpara>
|
||
<simpara>Applications hosted on the Google Cloud Platform include trace IDs under the <literal>x-cloud-trace-context</literal> header, which will be included in log entries.
|
||
However, if Sleuth is used the trace ID will be picked up from the MDC.</simpara>
|
||
</section>
|
||
<section xml:id="_logback_support">
|
||
<title>Logback Support</title>
|
||
<simpara>Currently, only Logback is supported and there are 2 possibilities to log to Stackdriver via this library with Logback: via direct API calls and through JSON-formatted console logs.</simpara>
|
||
<section xml:id="_log_via_api">
|
||
<title>Log via API</title>
|
||
<simpara>A Stackdriver appender is available using <literal>org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml</literal>.
|
||
This appender builds a Stackdriver Logging log entry from a JUL or Logback log entry, adds a trace ID to it and sends it to Stackdriver Logging.</simpara>
|
||
<simpara><literal>STACKDRIVER_LOG_NAME</literal> and <literal>STACKDRIVER_LOG_FLUSH_LEVEL</literal> environment variables can be used to customize the <literal>STACKDRIVER</literal> appender.</simpara>
|
||
<simpara>Your configuration may then look like this:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><configuration>
|
||
<include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" />
|
||
|
||
<root level="INFO">
|
||
<appender-ref ref="STACKDRIVER" />
|
||
</root>
|
||
</configuration></programlisting>
|
||
<simpara>If you want to have more control over the log output, you can further configure the appender.
|
||
The following properties are available:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="3">
|
||
<colspec colname="col_1" colwidth="33.3333*"/>
|
||
<colspec colname="col_2" colwidth="33.3333*"/>
|
||
<colspec colname="col_3" colwidth="33.3334*"/>
|
||
<thead>
|
||
<row>
|
||
<entry align="left" valign="top">Property</entry>
|
||
<entry align="left" valign="top">Default Value</entry>
|
||
<entry align="left" valign="top">Description</entry>
|
||
</row>
|
||
</thead>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>log</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>spring.log</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The Stackdriver Log name.
|
||
This can also be set via the <literal>STACKDRIVER_LOG_NAME</literal> environmental variable.</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>flushLevel</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>WARN</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>If a log entry with this level is encountered, trigger a flush of locally buffered log to Stackdriver Logging.
|
||
This can also be set via the <literal>STACKDRIVER_LOG_FLUSH_LEVEL</literal> environmental variable.</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_log_via_console">
|
||
<title>Log via Console</title>
|
||
<simpara>For Logback, a <literal>org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml</literal> file is made available for import to make it easier to configure the JSON Logback appender.</simpara>
|
||
<simpara>Your configuration may then look something like this:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><configuration>
|
||
<include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml" />
|
||
|
||
<root level="INFO">
|
||
<appender-ref ref="CONSOLE_JSON" />
|
||
</root>
|
||
</configuration></programlisting>
|
||
<simpara>If your application is running on Google Kubernetes Engine, Google Compute Engine or Google App Engine Flexible, your console logging is automatically saved to Google Stackdriver Logging.
|
||
Therefore, you can just include <literal>org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml</literal> in your logging configuration, which logs JSON entries to the console.
|
||
The trace id will be set correctly.</simpara>
|
||
<simpara>If you want to have more control over the log output, you can further configure the appender.
|
||
The following properties are available:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="3">
|
||
<colspec colname="col_1" colwidth="33.3333*"/>
|
||
<colspec colname="col_2" colwidth="33.3333*"/>
|
||
<colspec colname="col_3" colwidth="33.3334*"/>
|
||
<thead>
|
||
<row>
|
||
<entry align="left" valign="top">Property</entry>
|
||
<entry align="left" valign="top">Default Value</entry>
|
||
<entry align="left" valign="top">Description</entry>
|
||
</row>
|
||
</thead>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>projectId</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>If not set, default value is determined in the following order:</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara><literal>SPRING_CLOUD_GCP_LOGGING_PROJECT_ID</literal> Environmental Variable.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Value of <literal>DefaultGcpProjectIdProvider.getProjectId()</literal></simpara>
|
||
</listitem>
|
||
</orderedlist></entry>
|
||
<entry align="left" valign="top"><simpara>This is used to generate fully qualified Stackdriver Trace ID format: <literal>projects/[PROJECT-ID]/traces/[TRACE-ID]</literal>.</simpara>
|
||
<simpara>This format is required to correlate trace between Stackdriver Trace and Stackdriver Logging.</simpara>
|
||
<simpara>If <literal>projectId</literal> is not set and cannot be determined, then it’ll log <literal>traceId</literal> without the fully qualified format.</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeTraceId</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the <literal>traceId</literal> be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeSpanId</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the <literal>spanId</literal> be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeLevel</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the severity be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeThreadName</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the thread name be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeMDC</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should all MDC properties be included.
|
||
The MDC properties <literal>X-B3-TraceId</literal>, <literal>X-B3-SpanId</literal> and <literal>X-Span-Export</literal> provided by Spring Sleuth will get excluded as they get handled separately</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeLoggerName</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the name of the logger be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeFormattedMessage</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the formatted log message be included.</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeExceptionInMessage</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the stacktrace be appended to the formatted log message.
|
||
This setting is only evaluated if <literal>includeFormattedMessage</literal> is <literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeContextName</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the logging context be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeMessage</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>false</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the log message with blank placeholders be included</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>includeException</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>false</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Should the stacktrace be included as a own field</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<simpara>This is an example of such an Logback configuration:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><configuration >
|
||
<property name="projectId" value="${projectId:-${GOOGLE_CLOUD_PROJECT}}"/>
|
||
|
||
<appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
|
||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||
<layout class="org.springframework.cloud.gcp.logging.StackdriverJsonLayout">
|
||
<projectId>${projectId}</projectId>
|
||
|
||
<!--<includeTraceId>true</includeTraceId>-->
|
||
<!--<includeSpanId>true</includeSpanId>-->
|
||
<!--<includeLevel>true</includeLevel>-->
|
||
<!--<includeThreadName>true</includeThreadName>-->
|
||
<!--<includeMDC>true</includeMDC>-->
|
||
<!--<includeLoggerName>true</includeLoggerName>-->
|
||
<!--<includeFormattedMessage>true</includeFormattedMessage>-->
|
||
<!--<includeExceptionInMessage>true</includeExceptionInMessage>-->
|
||
<!--<includeContextName>true</includeContextName>-->
|
||
<!--<includeMessage>false</includeMessage>-->
|
||
<!--<includeException>false</includeException>-->
|
||
</layout>
|
||
</encoder>
|
||
</appender>
|
||
</configuration></programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_sample_7">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-logging-sample">Sample Spring Boot Application</link> is provided to show how to use the Cloud logging starter.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_cloud_config">
|
||
<title>Spring Cloud Config</title>
|
||
<simpara>Spring Cloud GCP makes it possible to use the <link xl:href="https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/">Google Runtime Configuration API</link> as a <link xl:href="https://cloud.spring.io/spring-cloud-config/">Spring Cloud Config</link> server to remotely store your application configuration data.</simpara>
|
||
<simpara>The Spring Cloud GCP Config support is provided via its own Spring Boot starter.
|
||
It enables the use of the Google Runtime Configuration API as a source for Spring Boot configuration properties.</simpara>
|
||
<note>
|
||
<simpara>The Google Cloud Runtime Configuration service is in beta status.</simpara>
|
||
</note>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-config</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-config'
|
||
}</screen>
|
||
<section xml:id="_configuration_3">
|
||
<title>Configuration</title>
|
||
<simpara>The following parameters are configurable in Spring Cloud GCP Config:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables the Config client</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>false</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.name</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Name of your application</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Value of the <literal>spring.application.name</literal> property.
|
||
If none, <literal>application</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.profile</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Active profile</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Value of the <literal>spring.profiles.active</literal> property.
|
||
If more than a single profile, last one is chosen</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.timeout-millis</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Timeout in milliseconds for connecting to the Google Runtime Configuration API</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>60000</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.project-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>GCP project ID where the Google Runtime Configuration API is hosted</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>OAuth2 credentials for authenticating with the Google Runtime Configuration API</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded OAuth2 credentials for authenticating with the Google Runtime Configuration API</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.config.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://developers.google.com/identity/protocols/googlescopes">OAuth2 scope</link> for Spring Cloud GCP Config credentials</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/cloudruntimeconfig">https://www.googleapis.com/auth/cloudruntimeconfig</link></simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<note>
|
||
<simpara>These properties should be specified in a <link xl:href="http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_the_bootstrap_application_context"><literal>bootstrap.yml</literal>/<literal>bootstrap.properties</literal></link> file, rather than the usual <literal>applications.yml</literal>/<literal>application.properties</literal>.</simpara>
|
||
</note>
|
||
<note>
|
||
<simpara>Core properties, as described in <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link>, do not apply to Spring Cloud GCP Config.</simpara>
|
||
</note>
|
||
</section>
|
||
<section xml:id="_quick_start">
|
||
<title>Quick start</title>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Create a configuration in the Google Runtime Configuration API that is called <literal>${spring.application.name}_${spring.profiles.active}</literal>.
|
||
In other words, if <literal>spring.application.name</literal> is <literal>myapp</literal> and <literal>spring.profiles.active</literal> is <literal>prod</literal>, the configuration should be called <literal>myapp_prod</literal>.</simpara>
|
||
<simpara>In order to do that, you should have the <link xl:href="https://cloud.google.com/sdk/">Google Cloud SDK</link> installed, own a Google Cloud Project and run the following command:</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
<screen>gcloud init # if this is your first Google Cloud SDK run.
|
||
gcloud beta runtime-config configs create myapp_prod
|
||
gcloud beta runtime-config configs variables set myapp.queue-size 25 --config-name myapp_prod</screen>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Configure your <literal>bootstrap.properties</literal> file with your application’s configuration data:</simpara>
|
||
<screen>spring.application.name=myapp
|
||
spring.profiles.active=prod</screen>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Add the <literal>@ConfigurationProperties</literal> annotation to a Spring-managed bean:</simpara>
|
||
<screen>@Component
|
||
@ConfigurationProperties("myapp")
|
||
public class SampleConfig {
|
||
|
||
private int queueSize;
|
||
|
||
public int getQueueSize() {
|
||
return this.queueSize;
|
||
}
|
||
|
||
public void setQueueSize(int queueSize) {
|
||
this.queueSize = queueSize;
|
||
}
|
||
}</screen>
|
||
</listitem>
|
||
</orderedlist>
|
||
<simpara>When your Spring application starts, the <literal>queueSize</literal> field value will be set to 25 for the above <literal>SampleConfig</literal> bean.</simpara>
|
||
</section>
|
||
<section xml:id="_refreshing_the_configuration_at_runtime">
|
||
<title>Refreshing the configuration at runtime</title>
|
||
<simpara><link xl:href="http://cloud.spring.io/spring-cloud-static/docs/1.0.x/spring-cloud.html#_endpoints">Spring Cloud</link> provides support to have configuration parameters be reloadable with the POST request to <literal>/actuator/refresh</literal> endpoint.</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Add the Spring Boot Actuator dependency:</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
<simpara>Maven coordinates:</simpara>
|
||
<screen><dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||
</dependency></screen>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator'
|
||
}</screen>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Add <literal>@RefreshScope</literal> to your Spring configuration class to have parameters be reloadable at runtime.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Add <literal>management.endpoints.web.exposure.include=refresh</literal> to your <literal>application.properties</literal> to allow unrestricted access to <literal>/actuator/refresh</literal>.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Update a property with <literal>gcloud</literal>:</simpara>
|
||
<literallayout class="monospaced">$ gcloud beta runtime-config configs variables set \
|
||
myapp.queue_size 200 \
|
||
--config-name myapp_prod</literallayout>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Send a POST request to the refresh endpoint:</simpara>
|
||
<literallayout class="monospaced">$ curl -XPOST http://myapp.host.com/actuator/refresh</literallayout>
|
||
</listitem>
|
||
</orderedlist>
|
||
</section>
|
||
<section xml:id="_sample_8">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-config-sample">sample application</link> and a <link xl:href="https://codelabs.developers.google.com/codelabs/cloud-spring-runtime-config/index.html">codelab</link> are available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_data_cloud_spanner">
|
||
<title>Spring Data Cloud Spanner</title>
|
||
<simpara><link xl:href="http://projects.spring.io/spring-data/">Spring Data</link> is an abstraction for storing and retrieving POJOs in numerous storage technologies.
|
||
Spring Cloud GCP adds Spring Data support for <link xl:href="http://cloud.google.com/spanner/">Google Cloud Spanner</link>.</simpara>
|
||
<simpara>Maven coordinates for this module only, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-data-spanner</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner'
|
||
}</screen>
|
||
<simpara>We provide a <link xl:href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner">Spring Boot Starter for Spring Data Spanner</link>, with which you can leverage our recommended auto-configuration setup.
|
||
To use the starter, see the coordinates see below.</simpara>
|
||
<simpara>Maven:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-data-spanner</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner'
|
||
}</screen>
|
||
<simpara>This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Spanner libraries as well.</simpara>
|
||
<section xml:id="_configuration_4">
|
||
<title>Configuration</title>
|
||
<simpara>To setup Spring Data Cloud Spanner, you have to configure the following:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Setup the connection details to Google Cloud Spanner.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Enable Spring Data Repositories (optional).</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_cloud_spanner_settings">
|
||
<title>Cloud Spanner settings</title>
|
||
<simpara>You can the use <link xl:href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner">Spring Boot Starter for Spring Data Spanner</link> 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:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.instance-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Cloud Spanner instance to use</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Yes</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.database</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Cloud Spanner database to use</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Yes</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.project-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>GCP project ID where the Google Cloud Spanner API is hosted, if different from the one in the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>OAuth2 credentials for authenticating with the
|
||
Google Cloud Spanner API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded OAuth2 credentials for authenticating with the
|
||
Google Cloud Spanner API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://developers.google.com/identity/protocols/googlescopes">OAuth2 scope</link> for Spring Cloud GCP
|
||
Cloud Spanner credentials</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/spanner.data">https://www.googleapis.com/auth/spanner.data</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>If <literal>true</literal>, then schema statements generated by <literal>SpannerSchemaUtils</literal> for tables with interleaved parent-child relationships will be "ON DELETE CASCADE".
|
||
The schema for the tables will be "ON DELETE NO ACTION" if <literal>false</literal>.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.numRpcChannels</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Number of gRPC channels used to connect to Cloud Spanner</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>4 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.prefetchChunks</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Number of chunks prefetched by Cloud Spanner for read and query</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>4 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.minSessions</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Minimum number of sessions maintained in the session pool</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.maxSessions</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum number of sessions session pool can have</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>400 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.maxIdleSessions</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Maximum number of idle sessions session pool will maintain</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.writeSessionsFraction</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Fraction of sessions to be kept prepared for write transactions</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>0.2 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.spanner.keepAliveIntervalMinutes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>How long to keep idle sessions alive</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>30 - Determined by Cloud Spanner client library</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_repository_settings">
|
||
<title>Repository settings</title>
|
||
<simpara>Spring Data Repositories can be configured via the <literal>@EnableSpannerRepositories</literal> annotation on your main <literal>@Configuration</literal> class.
|
||
With our Spring Boot Starter for Spring Data Cloud Spanner, <literal>@EnableSpannerRepositories</literal> 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 <link xl: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"><literal>@EnableSpannerRepositories</literal></link>.</simpara>
|
||
</section>
|
||
<section xml:id="_autoconfiguration">
|
||
<title>Autoconfiguration</title>
|
||
<simpara>Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>an instance of <literal>SpannerTemplate</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>an instance of <literal>SpannerDatabaseAdminTemplate</literal> for generating table schemas from object hierarchies and creating and deleting tables and databases</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>an instance of all user-defined repositories extending <literal>SpannerRepository</literal>, <literal>CrudRepository</literal>, <literal>PagingAndSortingRepository</literal>, when repositories are enabled</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>an instance of <literal>DatabaseClient</literal> from the Google Cloud Java Client for Spanner, for convenience and lower level API access</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_object_mapping">
|
||
<title>Object Mapping</title>
|
||
<simpara>Spring Data Cloud Spanner allows you to map domain POJOs to Cloud Spanner tables via annotations:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "traders")
|
||
public class Trader {
|
||
|
||
@PrimaryKey
|
||
@Column(name = "trader_id")
|
||
String traderId;
|
||
|
||
String firstName;
|
||
|
||
String lastName;
|
||
|
||
@NotMapped
|
||
Double temporaryNumber;
|
||
}</programlisting>
|
||
<simpara>Spring Data Cloud Spanner will ignore any property annotated with <literal>@NotMapped</literal>.
|
||
These properties will not be written to or read from Spanner.</simpara>
|
||
<section xml:id="_constructors">
|
||
<title>Constructors</title>
|
||
<simpara>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.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "traders")
|
||
public class Trader {
|
||
@PrimaryKey
|
||
@Column(name = "trader_id")
|
||
String traderId;
|
||
|
||
String firstName;
|
||
|
||
String lastName;
|
||
|
||
@NotMapped
|
||
Double temporaryNumber;
|
||
|
||
public Trader(String traderId, String firstName) {
|
||
this.traderId = traderId;
|
||
this.firstName = firstName;
|
||
}
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_table">
|
||
<title>Table</title>
|
||
<simpara>The <literal>@Table</literal> 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.</simpara>
|
||
<section xml:id="_spel_expressions_for_table_names">
|
||
<title>SpEL expressions for table names</title>
|
||
<simpara>In some cases, you might want the <literal>@Table</literal> table name to be determined dynamically.
|
||
To do that, you can use <link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions">Spring Expression Language</link>.</simpara>
|
||
<simpara>For example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "trades_#{tableNameSuffix}")
|
||
public class Trade {
|
||
// ...
|
||
}</programlisting>
|
||
<simpara>The table name will be resolved only if the <literal>tableNameSuffix</literal> value/bean in the Spring application context is defined.
|
||
For example, if <literal>tableNameSuffix</literal> has the value "123", the table name will resolve to <literal>trades_123</literal>.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_primary_keys">
|
||
<title>Primary Keys</title>
|
||
<simpara>For a simple table, you may only have a primary key consisting of a single column.
|
||
Even in that case, the <literal>@PrimaryKey</literal> annotation is required.
|
||
<literal>@PrimaryKey</literal> identifies the one or more ID properties corresponding to the primary key.</simpara>
|
||
<simpara>Spanner has first class support for composite primary keys of multiple columns.
|
||
You have to annotate all of your POJO’s fields that the primary key consists of with <literal>@PrimaryKey</literal> as below:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "trades")
|
||
public class Trade {
|
||
@PrimaryKey(keyOrder = 2)
|
||
@Column(name = "trade_id")
|
||
private String tradeId;
|
||
|
||
@PrimaryKey(keyOrder = 1)
|
||
@Column(name = "trader_id")
|
||
private String traderId;
|
||
|
||
private String action;
|
||
|
||
private Double price;
|
||
|
||
private Double shares;
|
||
|
||
private String symbol;
|
||
}</programlisting>
|
||
<simpara>The <literal>keyOrder</literal> parameter of <literal>@PrimaryKey</literal> 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:</simpara>
|
||
<programlisting language="sql" linenumbering="unnumbered">CREATE TABLE trades (
|
||
trader_id STRING(MAX),
|
||
trade_id STRING(MAX),
|
||
action STRING(15),
|
||
symbol STRING(10),
|
||
price FLOAT64,
|
||
shares FLOAT64
|
||
) PRIMARY KEY (trader_id, trade_id)</programlisting>
|
||
<simpara>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 <link xl:href="https://cloud.google.com/spanner/docs/schema-and-data-model#primary_keys">Spanner Primary Keys documentation</link> for a better understanding of primary keys and recommended practices.</simpara>
|
||
</section>
|
||
<section xml:id="_columns">
|
||
<title>Columns</title>
|
||
<simpara>All accessible properties on POJOs are automatically recognized as a Cloud Spanner column.
|
||
Column naming is generated by the <literal>PropertyNameFieldNamingStrategy</literal> by default defined on the <literal>SpannerMappingContext</literal> bean.
|
||
The <literal>@Column</literal> annotation optionally provides a different column name than that of the property and some other settings:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>name</literal> is the optional name of the column</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>spannerTypeMaxLength</literal> specifies for <literal>STRING</literal> and <literal>BYTES</literal> columns the maximum length.
|
||
This setting is only used when generating DDL schema statements based on domain types.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>nullable</literal> specifies if the column is created as <literal>NOT NULL</literal>.
|
||
This setting is only used when generating DDL schema statements based on domain types.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>spannerType</literal> 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.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>spannerCommitTimestamp</literal> 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.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
<section xml:id="_embedded_objects">
|
||
<title>Embedded Objects</title>
|
||
<simpara>If an object of type <literal>B</literal> is embedded as a property of <literal>A</literal>, then the columns of <literal>B</literal> will be saved in the same Cloud Spanner table as those of <literal>A</literal>.</simpara>
|
||
<simpara>If <literal>B</literal> has primary key columns, those columns will be included in the primary key of <literal>A</literal>. <literal>B</literal> 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.</simpara>
|
||
<simpara>For example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">class X {
|
||
@PrimaryKey
|
||
String grandParentId;
|
||
|
||
long age;
|
||
}
|
||
|
||
class A {
|
||
@PrimaryKey
|
||
@Embedded
|
||
X grandParent;
|
||
|
||
@PrimaryKey(keyOrder = 2)
|
||
String parentId;
|
||
|
||
String value;
|
||
}
|
||
|
||
@Table(name = "items")
|
||
class B {
|
||
@PrimaryKey
|
||
@Embedded
|
||
A parent;
|
||
|
||
@PrimaryKey(keyOrder = 2)
|
||
String id;
|
||
|
||
@Column(name = "child_value")
|
||
String value;
|
||
}</programlisting>
|
||
<simpara>Entities of <literal>B</literal> can be stored in a table defined as:</simpara>
|
||
<programlisting language="sql" linenumbering="unnumbered">CREATE TABLE items (
|
||
grandParentId STRING(MAX),
|
||
parentId STRING(MAX),
|
||
id STRING(MAX),
|
||
value STRING(MAX),
|
||
child_value STRING(MAX),
|
||
age INT64
|
||
) PRIMARY KEY (grandParentId, parentId, id)</programlisting>
|
||
<simpara>Note that embedded properties' column names must all be unique.</simpara>
|
||
</section>
|
||
<section xml:id="_relationships">
|
||
<title>Relationships</title>
|
||
<simpara>Spring Data Cloud Spanner supports parent-child relationships using the Cloud Spanner <link xl:href="https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables">parent-child interleaved table mechanism</link>.
|
||
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.</simpara>
|
||
<simpara>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.</simpara>
|
||
<simpara>For example, the following Java entities:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "Singers")
|
||
class Singer {
|
||
@PrimaryKey
|
||
long SingerId;
|
||
|
||
String FirstName;
|
||
|
||
String LastName;
|
||
|
||
byte[] SingerInfo;
|
||
|
||
@Interleaved
|
||
List<Album> albums;
|
||
}
|
||
|
||
@Table(name = "Albums")
|
||
class Album {
|
||
@PrimaryKey
|
||
long SingerId;
|
||
|
||
@PrimaryKey(keyOrder = 2)
|
||
long AlbumId;
|
||
|
||
String AlbumTitle;
|
||
}</programlisting>
|
||
<simpara>These classes can correspond to an existing pair of interleaved tables.
|
||
The <literal>@Interleaved</literal> annotation may be applied to <literal>Collection</literal> properties and the inner type is resolved as the child entity type.
|
||
The schema needed to create them can also be generated using the <literal>SpannerSchemaUtils</literal> and executed using the <literal>SpannerDatabaseAdminTemplate</literal>:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
SpannerSchemaUtils schemaUtils;
|
||
|
||
@Autowired
|
||
SpannerDatabaseAdminTemplate databaseAdmin;
|
||
...
|
||
|
||
// Get the create statmenets for all tables in the table structure rooted at Singer
|
||
List<String> createStrings = this.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.class);
|
||
|
||
// Create the tables and also create the database if necessary
|
||
this.databaseAdmin.executeDdlStrings(createStrings, true);</programlisting>
|
||
<simpara>The <literal>createStrings</literal> 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.</simpara>
|
||
<programlisting language="sql" linenumbering="unnumbered">CREATE TABLE Singers (
|
||
SingerId INT64 NOT NULL,
|
||
FirstName STRING(1024),
|
||
LastName STRING(1024),
|
||
SingerInfo BYTES(MAX),
|
||
) PRIMARY KEY (SingerId);
|
||
|
||
CREATE TABLE Albums (
|
||
SingerId INT64 NOT NULL,
|
||
AlbumId INT64 NOT NULL,
|
||
AlbumTitle STRING(MAX),
|
||
) PRIMARY KEY (SingerId, AlbumId),
|
||
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;</programlisting>
|
||
<simpara>The <literal>ON DELETE CASCADE</literal> clause indicates that Cloud Spanner will delete all Albums of a singer if the Singer is deleted.
|
||
The alternative is <literal>ON DELETE NO ACTION</literal>, where a Singer cannot be deleted until all of its Albums have already been deleted.
|
||
When using <literal>SpannerSchemaUtils</literal> to generate the schema strings, the <literal>spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade</literal> boolean setting determines if these schema are generated as <literal>ON DELETE CASCADE</literal> for <literal>true</literal> and <literal>ON DELETE NO ACTION</literal> for <literal>false</literal>.</simpara>
|
||
<simpara>Cloud Spanner restricts these relationships to 7 child layers.
|
||
A table may have multiple child tables.</simpara>
|
||
<simpara>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.</simpara>
|
||
</section>
|
||
<section xml:id="_supported_types">
|
||
<title>Supported Types</title>
|
||
<simpara>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.</simpara>
|
||
<simpara>Natively supported types:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.ByteArray</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.Date</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.Timestamp</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Boolean</literal>, <literal>boolean</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Double</literal>, <literal>double</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Long</literal>, <literal>long</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Integer</literal>, <literal>int</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.String</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>double[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>long[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>boolean[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.util.Date</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.util.Instant</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.sql.Date</literal></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
<section xml:id="_lists">
|
||
<title>Lists</title>
|
||
<simpara>Spanner supports <literal>ARRAY</literal> types for columns.
|
||
<literal>ARRAY</literal> columns are mapped to <literal>List</literal> fields in POJOS.</simpara>
|
||
<simpara>Example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Double> curve;</programlisting>
|
||
<simpara>The types inside the lists can be any singular property type.</simpara>
|
||
</section>
|
||
<section xml:id="_lists_of_structs">
|
||
<title>Lists of Structs</title>
|
||
<simpara>Cloud Spanner queries can <link xl:href="https://cloud.google.com/spanner/docs/query-syntax#using-structs-with-select">construct STRUCT values</link> that appear as columns in the result.
|
||
Cloud Spanner requires STRUCT values appear in ARRAYs at the root level: <literal>SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users</literal>.</simpara>
|
||
<simpara>Spring Data Cloud Spanner will attempt to read the column STRUCT values into a property that is an <literal>Iterable</literal> of an entity type compatible with the schema of the column STRUCT value.</simpara>
|
||
<simpara>For the previous array-select example, the following property can be mapped with the constructed <literal>ARRAY<STRUCT></literal> column: <literal>List<TwoInts> pair;</literal> where the <literal>TwoInts</literal> type is defined:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">class TwoInts {
|
||
|
||
int val1;
|
||
|
||
int val2;
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_custom_types">
|
||
<title>Custom types</title>
|
||
<simpara>Custom converters can be used to extend the type support for user defined types.</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Converters need to implement the <literal>org.springframework.core.convert.converter.Converter</literal> interface in both directions.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The user defined type needs to be mapped to one of the basic types supported by Spanner:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.ByteArray</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.Date</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.Timestamp</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Boolean</literal>, <literal>boolean</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Double</literal>, <literal>double</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Long</literal>, <literal>long</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.String</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>double[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>long[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>boolean[]</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>enum</literal> types</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>An instance of both Converters needs to be passed to a <literal>ConverterAwareMappingSpannerEntityProcessor</literal>, which then has to be made available as a <literal>@Bean</literal> for <literal>SpannerEntityProcessor</literal>.</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
<simpara>For example:</simpara>
|
||
<simpara>We would like to have a field of type <literal>Person</literal> on our <literal>Trade</literal> POJO:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Table(name = "trades")
|
||
public class Trade {
|
||
//...
|
||
Person person;
|
||
//...
|
||
}</programlisting>
|
||
<simpara>Where Person is a simple class:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public class Person {
|
||
|
||
public String firstName;
|
||
public String lastName;
|
||
|
||
}</programlisting>
|
||
<simpara>We have to define the two converters:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> public class PersonWriteConverter implements Converter<Person, String> {
|
||
|
||
@Override
|
||
public String convert(Person person) {
|
||
return person.firstName + " " + person.lastName;
|
||
}
|
||
}
|
||
|
||
public class PersonReadConverter implements Converter<String, Person> {
|
||
|
||
@Override
|
||
public Person convert(String s) {
|
||
Person person = new Person();
|
||
person.firstName = s.split(" ")[0];
|
||
person.lastName = s.split(" ")[1];
|
||
return person;
|
||
}
|
||
}</programlisting>
|
||
<simpara>That will be configured in our <literal>@Configuration</literal> file:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Configuration
|
||
public class ConverterConfiguration {
|
||
|
||
@Bean
|
||
public SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) {
|
||
return new ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext,
|
||
Arrays.asList(new PersonWriteConverter()),
|
||
Arrays.asList(new PersonReadConverter()));
|
||
}
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_custom_converter_for_struct_array_columns">
|
||
<title>Custom Converter for Struct Array Columns</title>
|
||
<simpara>If a <literal>Converter<Struct, A></literal> is provided, then properties of type <literal>List<A></literal> can be used in your entity types.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_spanner_operations_template">
|
||
<title>Spanner Operations & Template</title>
|
||
<simpara><literal>SpannerOperations</literal> and its implementation, <literal>SpannerTemplate</literal>, provides the Template pattern familiar to Spring developers.
|
||
It provides:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Resource management</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>One-stop-shop to Spanner operations with the Spring Data POJO mapping and conversion features</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Exception conversion</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Using the <literal>autoconfigure</literal> provided by our Spring Boot Starter for Spanner, your Spring application context will contain a fully configured <literal>SpannerTemplate</literal> object that you can easily autowire in your application:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@SpringBootApplication
|
||
public class SpannerTemplateExample {
|
||
|
||
@Autowired
|
||
SpannerTemplate spannerTemplate;
|
||
|
||
public void doSomething() {
|
||
this.spannerTemplate.delete(Trade.class, KeySet.all());
|
||
//...
|
||
Trade t = new Trade();
|
||
//...
|
||
this.spannerTemplate.insert(t);
|
||
//...
|
||
List<Trade> tradesByAction = spannerTemplate.findAll(Trade.class);
|
||
//...
|
||
}
|
||
}</programlisting>
|
||
<simpara>The Template API provides convenience methods for:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://cloud.google.com/spanner/docs/reads">Reads</link>, and by providing SpannerReadOptions and
|
||
SpannerQueryOptions</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Stale read</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Read with secondary indices</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Read with limits and offsets</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Read with sorting</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://cloud.google.com/spanner/docs/reads#execute_a_query">Queries</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>DML operations (delete, insert, update, upsert)</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Partial reads</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>You can define a set of columns to be read into your entity</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Partial writes</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Persist only a few properties from your entity</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Read-only transactions</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Locking read-write transactions</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_sql_query">
|
||
<title>SQL Query</title>
|
||
<simpara>Cloud Spanner has SQL support for running read-only queries.
|
||
All the query related methods start with <literal>query</literal> on <literal>SpannerTemplate</literal>.
|
||
Using <literal>SpannerTemplate</literal> you can execute SQL queries that map to POJOs:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"));</programlisting>
|
||
</section>
|
||
<section xml:id="_read">
|
||
<title>Read</title>
|
||
<simpara>Spanner exposes a <link xl:href="https://cloud.google.com/spanner/docs/reads">Read API</link> for reading single row or multiple rows in a table or in a secondary index.</simpara>
|
||
<simpara>Using <literal>SpannerTemplate</literal> you can execute reads, for example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> trades = this.spannerTemplate.readAll(Trade.class);</programlisting>
|
||
<simpara>Main benefit of reads over queries is reading multiple rows of a certain pattern of keys is much easier using the features of the <link xl:href="https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-spanner/src/main/java/com/google/cloud/spanner/KeySet.java"><literal>KeySet</literal></link> class.</simpara>
|
||
</section>
|
||
<section xml:id="_advanced_reads">
|
||
<title>Advanced reads</title>
|
||
<section xml:id="_stale_read">
|
||
<title>Stale read</title>
|
||
<simpara>All reads and queries are <emphasis role="strong">strong reads</emphasis> by default.
|
||
A <emphasis role="strong">strong read</emphasis> 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 <emphasis role="strong">stale read</emphasis> 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 <literal>SpannerTemplate</literal> you can specify the <literal>Timestamp</literal> by setting it on <literal>SpannerQueryOptions</literal> or <literal>SpannerReadOptions</literal> to the appropriate read or query methods:</simpara>
|
||
<simpara>Reads:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// a read with options:
|
||
SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setTimestamp(Timestamp.now());
|
||
List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);</programlisting>
|
||
<simpara>Queries:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// a query with options:
|
||
SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp(Timestamp.now());
|
||
List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);</programlisting>
|
||
</section>
|
||
<section xml:id="_read_from_a_secondary_index">
|
||
<title>Read from a secondary index</title>
|
||
<simpara>Using a <link xl:href="https://cloud.google.com/spanner/docs/secondary-indexes">secondary index</link> is available for Reads via the Template API and it is also implicitly available via SQL for Queries.</simpara>
|
||
<simpara>The following shows how to read rows from a table using a <link xl:href="https://cloud.google.com/spanner/docs/secondary-indexes">secondary index</link> simply by setting <literal>index</literal> on <literal>SpannerReadOptions</literal>:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setIndex("TradesByTrader");
|
||
List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);</programlisting>
|
||
</section>
|
||
<section xml:id="_read_with_offsets_and_limits">
|
||
<title>Read with offsets and limits</title>
|
||
<simpara>Limits and offsets are only supported by Queries.
|
||
The following will get only the first two rows of the query:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setLimit(2).setOffset(3);
|
||
List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);</programlisting>
|
||
<simpara>Note that the above is equivalent of executing <literal>SELECT * FROM trades LIMIT 2 OFFSET 3</literal>.</simpara>
|
||
</section>
|
||
<section xml:id="_sorting">
|
||
<title>Sorting</title>
|
||
<simpara>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:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> trades = this.spannerTemplate.queryAll(Trade.class, Sort.by("action"));</programlisting>
|
||
<simpara>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:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">Sort.by(Order.desc("action").ignoreCase())</programlisting>
|
||
</section>
|
||
<section xml:id="_partial_read">
|
||
<title>Partial read</title>
|
||
<simpara>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.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT action, symbol FROM trades"),
|
||
new SpannerQueryOptions().setAllowMissingResultSetColumns(true));</programlisting>
|
||
<simpara>If the setting is set to <literal>false</literal>, then an exception will be thrown if there are missing columns in the query result.</simpara>
|
||
</section>
|
||
<section xml:id="_summary_of_options_for_query_vs_read">
|
||
<title>Summary of options for Query vs Read</title>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="3">
|
||
<colspec colname="col_1" colwidth="33.3333*"/>
|
||
<colspec colname="col_2" colwidth="33.3333*"/>
|
||
<colspec colname="col_3" colwidth="33.3334*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Feature</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Query supports it</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Read supports it</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>SQL</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Partial read</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Limits</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Offsets</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Secondary index</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Read using index range</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Sorting</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>yes</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>no</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_write_update">
|
||
<title>Write / Update</title>
|
||
<simpara>The write methods of <literal>SpannerOperations</literal> accept a POJO and writes all of its properties to Spanner.
|
||
The corresponding Spanner table and entity metadata is obtained from the given object’s actual type.</simpara>
|
||
<simpara>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.</simpara>
|
||
<section xml:id="_insert">
|
||
<title>Insert</title>
|
||
<simpara>The <literal>insert</literal> method of <literal>SpannerOperations</literal> accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if a row with the POJO’s primary key already exists in the table.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">Trade t = new Trade();
|
||
this.spannerTemplate.insert(t);</programlisting>
|
||
</section>
|
||
<section xml:id="_update">
|
||
<title>Update</title>
|
||
<simpara>The <literal>update</literal> method of <literal>SpannerOperations</literal> accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if the POJO’s primary key does not already exist in the table.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// t was retrieved from a previous operation
|
||
this.spannerTemplate.update(t);</programlisting>
|
||
</section>
|
||
<section xml:id="_upsert">
|
||
<title>Upsert</title>
|
||
<simpara>The <literal>upsert</literal> method of <literal>SpannerOperations</literal> accepts a POJO and writes all of its properties to Spanner using update-or-insert.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// t was retrieved from a previous operation or it's new
|
||
this.spannerTemplate.upsert(t);</programlisting>
|
||
</section>
|
||
<section xml:id="_partial_update">
|
||
<title>Partial Update</title>
|
||
<simpara>The update methods of <literal>SpannerOperations</literal> operate by default on all properties within the given object, but also accept <literal>String[]</literal> and <literal>Optional<Set<String>></literal> of column names.
|
||
If the <literal>Optional</literal> 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.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">// t was retrieved from a previous operation or it's new
|
||
this.spannerTemplate.update(t, "symbol", "action");</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_dml">
|
||
<title>DML</title>
|
||
<simpara>DML statements can be executed using <literal>SpannerOperations.executeDmlStatement</literal>.
|
||
Inserts, updates, and deletions can affect any number of rows and entities.</simpara>
|
||
</section>
|
||
<section xml:id="_transactions">
|
||
<title>Transactions</title>
|
||
<simpara><literal>SpannerOperations</literal> provides methods to run <literal>java.util.Function</literal> objects within a single transaction while making available the read and write methods from <literal>SpannerOperations</literal>.</simpara>
|
||
<section xml:id="_readwrite_transaction">
|
||
<title>Read/Write Transaction</title>
|
||
<simpara>Read and write transactions are provided by <literal>SpannerOperations</literal> via the <literal>performReadWriteTransaction</literal> method:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
SpannerOperations mySpannerOperations;
|
||
|
||
public String doWorkInsideTransaction() {
|
||
return mySpannerOperations.performReadWriteTransaction(
|
||
transActionSpannerOperations -> {
|
||
// Work with transActionSpannerOperations here.
|
||
// It is also a SpannerOperations object.
|
||
|
||
return "transaction completed";
|
||
}
|
||
);
|
||
}</programlisting>
|
||
<simpara>The <literal>performReadWriteTransaction</literal> method accepts a <literal>Function</literal> that is provided an instance of a <literal>SpannerOperations</literal> 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 <literal>SpannerOperations</literal> with a few exceptions:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Its read functionality cannot perform stale reads, because all reads and writes happen at the single point in time of the transaction.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>It cannot perform sub-transactions via <literal>performReadWriteTransaction</literal> or <literal>performReadOnlyTransaction</literal>.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>As these read-write transactions are locking, it is recommended that you use the <literal>performReadOnlyTransaction</literal> if your function does not perform any writes.</simpara>
|
||
</section>
|
||
<section xml:id="_read_only_transaction">
|
||
<title>Read-only Transaction</title>
|
||
<simpara>The <literal>performReadOnlyTransaction</literal> method is used to perform read-only transactions using a <literal>SpannerOperations</literal>:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
SpannerOperations mySpannerOperations;
|
||
|
||
public String doWorkInsideTransaction() {
|
||
return mySpannerOperations.performReadOnlyTransaction(
|
||
transActionSpannerOperations -> {
|
||
// Work with transActionSpannerOperations here.
|
||
// It is also a SpannerOperations object.
|
||
|
||
return "transaction completed";
|
||
}
|
||
);
|
||
}</programlisting>
|
||
<simpara>The <literal>performReadOnlyTransaction</literal> method accepts a <literal>Function</literal> that is provided an instance of a
|
||
<literal>SpannerOperations</literal> object.
|
||
This method also accepts a <literal>ReadOptions</literal> 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 <literal>SpannerOperations</literal> with
|
||
a few exceptions:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>It cannot perform sub-transactions via <literal>performReadWriteTransaction</literal> or <literal>performReadOnlyTransaction</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>It cannot perform any write operations.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>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.</simpara>
|
||
</section>
|
||
<section xml:id="_declarative_transactions_with_transactional_annotation">
|
||
<title>Declarative Transactions with @Transactional Annotation</title>
|
||
<simpara>This feature requires a bean of <literal>SpannerTransactionManager</literal>, which is provided when using <literal>spring-cloud-gcp-starter-data-spanner</literal>.</simpara>
|
||
<simpara><literal>SpannerTemplate</literal> and <literal>SpannerRepository</literal> support running methods with the <literal>@Transactional</literal> [annotation](<link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative">https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative</link>) as transactions.
|
||
If a method annotated with <literal>@Transactional</literal> calls another method also annotated, then both methods will work within the same transaction.
|
||
<literal>performReadOnlyTransaction</literal> and <literal>performReadWriteTransaction</literal> cannot be used in <literal>@Transactional</literal> annotated methods because Cloud Spanner does not support transactions within transactions.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_dml_statements">
|
||
<title>DML Statements</title>
|
||
<simpara><literal>SpannerTemplate</literal> supports [DML](<link xl:href="https://cloud.google.com/spanner/docs/dml-tasks">https://cloud.google.com/spanner/docs/dml-tasks</link>) <literal>Statements</literal>.
|
||
DML statements can be executed in transactions via <literal>performReadWriteTransaction</literal> or using the <literal>@Transactional</literal> annotation.</simpara>
|
||
<simpara>When DML statements are executed outside of transactions, they are executed in [partitioned-mode](<link xl:href="https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml">https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml</link>).</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_repositories">
|
||
<title>Repositories</title>
|
||
<simpara><link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories">Spring Data Repositories</link> are a powerful abstraction that can save you a lot of boilerplate code.</simpara>
|
||
<simpara>For example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TraderRepository extends SpannerRepository<Trader, String> {
|
||
}</programlisting>
|
||
<simpara>Spring Data generates a working implementation of the specified interface, which can be conveniently autowired into an application.</simpara>
|
||
<simpara>The <literal>Trader</literal> type parameter to <literal>SpannerRepository</literal> refers to the underlying domain type.
|
||
The second type parameter, <literal>String</literal> in this case, refers to the type of the key of the domain type.</simpara>
|
||
<simpara>For POJOs with a composite primary key, this ID type parameter can be any descendant of <literal>Object[]</literal> compatible with all primary key properties, any descendant of <literal>Iterable</literal>, or <literal>com.google.cloud.spanner.Key</literal>.
|
||
If the domain POJO type only has a single primary key column, then the primary key property type can be used or the <literal>Key</literal> type.</simpara>
|
||
<simpara>For example in case of Trades, that belong to a Trader, <literal>TradeRepository</literal> would look like this:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeRepository extends SpannerRepository<Trade, String[]> {
|
||
|
||
}</programlisting>
|
||
<programlisting language="java" linenumbering="unnumbered">public class MyApplication {
|
||
|
||
@Autowired
|
||
SpannerTemplate spannerTemplate;
|
||
|
||
@Autowired
|
||
StudentRepository studentRepository;
|
||
|
||
public void demo() {
|
||
|
||
this.tradeRepository.deleteAll();
|
||
String traderId = "demo_trader";
|
||
Trade t = new Trade();
|
||
t.symbol = stock;
|
||
t.action = action;
|
||
t.traderId = traderId;
|
||
t.price = 100.0;
|
||
t.shares = 12345.6;
|
||
this.spannerTemplate.insert(t);
|
||
|
||
Iterable<Trade> allTrades = this.tradeRepository.findAll();
|
||
|
||
int count = this.tradeRepository.countByAction("BUY");
|
||
|
||
}
|
||
}</programlisting>
|
||
<section xml:id="_crud_repository">
|
||
<title>CRUD Repository</title>
|
||
<simpara><literal>CrudRepository</literal> methods work as expected, with one thing Spanner specific: the <literal>save</literal> and <literal>saveAll</literal> methods work as update-or-insert.</simpara>
|
||
</section>
|
||
<section xml:id="_paging_and_sorting_repository">
|
||
<title>Paging and Sorting Repository</title>
|
||
<simpara>You can also use <literal>PagingAndSortingRepository</literal> with Spanner Spring Data.
|
||
The sorting and pageable <literal>findAll</literal> 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.</simpara>
|
||
</section>
|
||
<section xml:id="_spanner_repository">
|
||
<title>Spanner Repository</title>
|
||
<simpara>The <literal>SpannerRepository</literal> extends the <literal>PagingAndSortingRepository</literal>, but adds the read-only and the read-write transaction functionality provided by Spanner.
|
||
These transactions work very similarly to those of <literal>SpannerOperations</literal>, but is specific to the repository’s domain type and provides repository functions instead of template functions.</simpara>
|
||
<simpara>For example, this is a read-write transaction:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
SpannerRepository myRepo;
|
||
|
||
public String doWorkInsideTransaction() {
|
||
return myRepo.performReadOnlyTransaction(
|
||
transactionSpannerRepo -> {
|
||
// Work with the single-transaction transactionSpannerRepo here.
|
||
// This is a SpannerRepository object.
|
||
|
||
return "transaction completed";
|
||
}
|
||
);
|
||
}</programlisting>
|
||
<simpara>When creating custom repositories for your own domain types and query methods, you can extend <literal>SpannerRepository</literal> to access Cloud Spanner-specific features as well as all features from <literal>PagingAndSortingRepository</literal> and <literal>CrudRepository</literal>.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_query_methods">
|
||
<title>Query Methods</title>
|
||
<simpara><literal>SpannerRepository</literal> 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 <literal>Struct</literal> or POJOs.
|
||
If a POJO is given as a parameter, it will be converted to a <literal>Struct</literal> with the same type-conversion logic as used to create write mutations.
|
||
Comparisons using Struct parameters are limited to <link xl:href="https://cloud.google.com/spanner/docs/data-types#limited-comparisons-for-struct">what is available with Cloud Spanner</link>.</simpara>
|
||
<section xml:id="_query_methods_by_convention">
|
||
<title>Query methods by convention</title>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeRepository extends SpannerRepository<Trade, String[]> {
|
||
List<Trade> findByAction(String action);
|
||
|
||
int countByAction(String action);
|
||
|
||
// Named methods are powerful, but can get unwieldy
|
||
List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(
|
||
String action, String symbol, String traderId);
|
||
}</programlisting>
|
||
<simpara>In the example above, the <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories.query-methods">query methods</link> in <literal>TradeRepository</literal> are generated based on the name of the methods, using the <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation">Spring Data Query creation naming convention</link>.</simpara>
|
||
<simpara><literal>List<Trade> findByAction(String action)</literal> would translate to a <literal>SELECT * FROM trades WHERE action = ?</literal>.</simpara>
|
||
<simpara>The function <literal>List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId);</literal> will be translated as the equivalent of this SQL query:</simpara>
|
||
<programlisting language="sql" linenumbering="unnumbered">SELECT DISTINCT * FROM trades
|
||
WHERE ACTION = ? AND LOWER(SYMBOL) = LOWER(?) AND TRADER_ID = ?
|
||
ORDER BY SYMBOL DESC
|
||
LIMIT 3</programlisting>
|
||
<simpara>The following filter options are supported:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Equality</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Greater than or equals</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Greater than</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Less than or equals</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Less than</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Is null</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Is not null</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Is true</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Is false</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Like a string</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Not like a string</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Contains a string</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Not contains a string</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Note that the phrase <literal>SymbolIgnoreCase</literal> is translated to <literal>LOWER(SYMBOL) = LOWER(?)</literal> indicating a non-case-sensitive matching.
|
||
The <literal>IgnoreCase</literal> 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.</simpara>
|
||
<simpara>The <literal>Like</literal> or <literal>NotLike</literal> naming conventions:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> findBySymbolLike(String symbolFragment);</programlisting>
|
||
<simpara>The param <literal>symbolFragment</literal> can contain <link xl:href="https://cloud.google.com/spanner/docs/functions-and-operators#comparison-operators">wildcard characters</link> for string matching such as <literal>_</literal> and <literal>%</literal>.</simpara>
|
||
<simpara>The <literal>Contains</literal> and <literal>NotContains</literal> naming conventions:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">List<Trade> findBySymbolContains(String symbolFragment);</programlisting>
|
||
<simpara>The param <literal>symbolFragment</literal> is a <link xl:href="https://cloud.google.com/spanner/docs/functions-and-operators#regexp_contains">regular expression</link> that is checked for occurrences.</simpara>
|
||
<simpara>Delete queries are also supported.
|
||
For example, query methods such as <literal>deleteByAction</literal> or <literal>removeByAction</literal> delete entities found by <literal>findByAction</literal>.
|
||
The delete operation happens in a single transaction.</simpara>
|
||
<simpara>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
|
||
* <literal>void</literal></simpara>
|
||
</section>
|
||
<section xml:id="_custom_sqldml_query_methods">
|
||
<title>Custom SQL/DML query methods</title>
|
||
<simpara>The example above for <literal>List<Trade> fetchByActionNamedQuery(String action)</literal> does not match the <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation">Spring Data Query creation naming convention</link>, so we have to map a parametrized Spanner SQL query to it.</simpara>
|
||
<simpara>The SQL query for the method can be mapped to repository methods in one of two ways:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>namedQueries</literal> properties file</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>using the <literal>@Query</literal> annotation</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>The names of the tags of the SQL correspond to the <literal>@Param</literal> annotated names of the method parameters.</simpara>
|
||
<simpara>Custom SQL query methods can accept a single <literal>Sort</literal> or <literal>Pageable</literal> parameter that is applied on top of any sorting or paging in the SQL:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> @Query("SELECT * FROM trades ORDER BY action DESC")
|
||
List<Trade> sortedTrades(Pageable pageable);
|
||
|
||
@Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1")
|
||
Trade sortedTopTrade(Pageable pageable);</programlisting>
|
||
<simpara>This can be used:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> List<Trade> customSortedTrades = tradeRepository.sortedTrades(PageRequest
|
||
.of(2, 2, org.springframework.data.domain.Sort.by(Order.asc("id"))));</programlisting>
|
||
<simpara>The results would be sorted by "id" in ascending order.</simpara>
|
||
<simpara>Your query method can also return non-entity types:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> @Query("SELECT COUNT(1) FROM trades WHERE action = @action")
|
||
int countByActionQuery(String action);
|
||
|
||
@Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)")
|
||
boolean existsByActionQuery(String action);
|
||
|
||
@Query("SELECT action FROM trades WHERE action = @action LIMIT 1")
|
||
String getFirstString(@Param("action") String action);
|
||
|
||
@Query("SELECT action FROM trades WHERE action = @action")
|
||
List<String> getFirstStringList(@Param("action") String action);</programlisting>
|
||
<simpara>DML statements can also be executed by query methods, but the only possible return value is a <literal>long</literal> representing the number of affected rows.
|
||
The <literal>dmlStatement</literal> boolean setting must be set on <literal>@Query</literal> to indicate that the query method is executed as a DML statement.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> @Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true)
|
||
long deleteByActionQuery(String action);</programlisting>
|
||
<section xml:id="_query_methods_with_named_queries_properties">
|
||
<title>Query methods with named queries properties</title>
|
||
<simpara>By default, the <literal>namedQueriesLocation</literal> attribute on <literal>@EnableSpannerRepositories</literal> points to the <literal>META-INF/spanner-named-queries.properties</literal> 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:</simpara>
|
||
<programlisting language="properties" linenumbering="unnumbered">Trade.fetchByActionNamedQuery=SELECT * FROM trades WHERE trades.action = @tag0</programlisting>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeRepository extends SpannerRepository<Trade, String[]> {
|
||
// This method uses the query from the properties file instead of one generated based on name.
|
||
List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_query_methods_with_annotation">
|
||
<title>Query methods with annotation</title>
|
||
<simpara>Using the <literal>@Query</literal> annotation:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeRepository extends SpannerRepository<Trade, String[]> {
|
||
@Query("SELECT * FROM trades WHERE trades.action = @tag0")
|
||
List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
|
||
}</programlisting>
|
||
<simpara>Table names can be used directly.
|
||
For example, "trades" in the above example.
|
||
Alternatively, table names can be resolved from the <literal>@Table</literal> annotation on domain classes as well.
|
||
In this case, the query should refer to table names with fully qualified class names between <literal>:</literal>
|
||
characters: <literal>:fully.qualified.ClassName:</literal>.
|
||
A full example would look like:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0")
|
||
List<Trade> fetchByActionNamedQuery(String action);</programlisting>
|
||
<simpara>This allows table names evaluated with SpEL to be used in custom queries.</simpara>
|
||
<simpara>SpEL can also be used to provide SQL parameters:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0
|
||
AND price > #{#priceRadius * -1} AND price < #{#priceRadius * 2}")
|
||
List<Trade> fetchByActionNamedQuery(String action, Double priceRadius);</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_projections">
|
||
<title>Projections</title>
|
||
<simpara>Spring Data Spanner supports <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#projections">projections</link>.
|
||
You can define projection interfaces based on domain types and add query methods that return them in your repository:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeProjection {
|
||
|
||
String getAction();
|
||
|
||
@Value("#{target.symbol + ' ' + target.action}")
|
||
String getSymbolAndAction();
|
||
}
|
||
|
||
public interface TradeRepository extends SpannerRepository<Trade, Key> {
|
||
|
||
List<Trade> findByTraderId(String traderId);
|
||
|
||
List<TradeProjection> findByAction(String action);
|
||
|
||
@Query("SELECT action, symbol FROM trades WHERE action = @action")
|
||
List<TradeProjection> findByQuery(String action);
|
||
}</programlisting>
|
||
<simpara>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.</simpara>
|
||
<simpara>Properties of projection types defined using SpEL use the fixed name <literal>target</literal> for the underlying domain object.
|
||
As a result accessing underlying properties take the form <literal>target.<property-name></literal>.</simpara>
|
||
</section>
|
||
<section xml:id="_rest_repositories">
|
||
<title>REST Repositories</title>
|
||
<simpara>When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-data-rest</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>If you prefer to configure parameters (such as path), you can use <literal>@RepositoryRestResource</literal> annotation:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
|
||
public interface TradeRepository extends SpannerRepository<Trade, String[]> {
|
||
}</programlisting>
|
||
<simpara>For example, you can retrieve all <literal>Trade</literal> objects in the repository by using <literal>curl http://<server>:<port>/trades</literal>, or any specific trade via <literal>curl http://<server>:<port>/trades/<trader_id>,<trade_id></literal>.</simpara>
|
||
<simpara>The separator between your primary key components, <literal>id</literal> and <literal>trader_id</literal> in this case, is a comma by default, but can be configured to any string not found in your key values by extending the <literal>SpannerKeyIdConverter</literal> class:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Component
|
||
class MySpecialIdConverter extends SpannerKeyIdConverter {
|
||
|
||
@Override
|
||
protected String getUrlIdSeparator() {
|
||
return ":";
|
||
}
|
||
}</programlisting>
|
||
<simpara>You can also write trades using <literal>curl -XPOST -H"Content-Type: application/json" -<link xl:href="mailto:d@test.json">d@test.json</link> http://<server>:<port>/trades/</literal> where the file <literal>test.json</literal> holds the JSON representation of a <literal>Trade</literal> object.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_database_and_schema_admin">
|
||
<title>Database and Schema Admin</title>
|
||
<simpara>Databases and tables inside Spanner instances can be created automatically from <literal>SpannerPersistentEntity</literal> objects:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
private SpannerSchemaUtils spannerSchemaUtils;
|
||
|
||
@Autowired
|
||
private SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate;
|
||
|
||
public void createTable(SpannerPersistentEntity entity) {
|
||
if(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){
|
||
|
||
// The boolean parameter indicates that the database will be created if it does not exist.
|
||
spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList(
|
||
spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true);
|
||
}
|
||
}</programlisting>
|
||
<simpara>Schemas can be generated for entire object hierarchies with interleaved relationships and composite keys.</simpara>
|
||
</section>
|
||
<section xml:id="_sample_9">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample">sample application</link> is available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_spring_data_cloud_datastore">
|
||
<title>Spring Data Cloud Datastore</title>
|
||
<simpara><link xl:href="http://projects.spring.io/spring-data/">Spring Data</link> is an abstraction for storing and retrieving POJOs in numerous storage technologies.
|
||
Spring Cloud GCP adds Spring Data support for <link xl:href="http://cloud.google.com/datastore/">Google Cloud Datastore</link>.</simpara>
|
||
<simpara>Maven coordinates for this module only, using <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/blob/master/spring-cloud-gcp-dependencies/pom.xml">Spring Cloud GCP BOM</link>:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-data-datastore</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-datastore'
|
||
}</screen>
|
||
<simpara>We provide a <link xl:href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-datastore">Spring Boot Starter for Spring Data Datastore</link>, with which you can use our recommended auto-configuration setup.
|
||
To use the starter, see the coordinates below.</simpara>
|
||
<simpara>Maven:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-datastore'
|
||
}</screen>
|
||
<simpara>This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Datastore libraries as well.</simpara>
|
||
<section xml:id="_configuration_5">
|
||
<title>Configuration</title>
|
||
<simpara>To setup Spring Data Cloud Datastore, you have to configure the following:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Setup the connection details to Google Cloud Datastore.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_cloud_datastore_settings">
|
||
<title>Cloud Datastore settings</title>
|
||
<simpara>You can the use <link xl:href="../spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-datastore">Spring Boot Starter for Spring Data Datastore</link> to autoconfigure Google Cloud Datastore 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:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Name</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Description</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Required</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Default value</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.enabled</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Enables the Cloud Datastore client</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>true</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.project-id</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>GCP project ID where the Google Cloud Datastore API is hosted, if different from the one in the <link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.credentials.location</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>OAuth2 credentials for authenticating with the
|
||
Google Cloud Datastore API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.credentials.encoded-key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Base64-encoded OAuth2 credentials for authenticating with the
|
||
Google Cloud Datastore API, if different from the ones in the
|
||
<link linkend="spring-cloud-gcp-core">Spring Cloud GCP Core Module</link></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.credentials.scopes</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://developers.google.com/identity/protocols/googlescopes">OAuth2 scope</link> for Spring Cloud GCP
|
||
Cloud Datastore credentials</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><link xl:href="https://www.googleapis.com/auth/datastore">https://www.googleapis.com/auth/datastore</link></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.datastore.namespace</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>The Cloud Datastore namespace to use</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>No</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>the Default namespace of Cloud Datastore in your GCP project</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_repository_settings_2">
|
||
<title>Repository settings</title>
|
||
<simpara>Spring Data Repositories can be configured via the <literal>@EnableDatastoreRepositories</literal> annotation on your main <literal>@Configuration</literal> class.
|
||
With our Spring Boot Starter for Spring Data Cloud Datastore, <literal>@EnableDatastoreRepositories</literal> 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 <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/blob/master/spring-cloud-gcp-data-datastore/src/main/java/org/springframework/cloud/gcp/data/datastore/repository/config/EnableDatastoreRepositories.java"><literal>@EnableDatastoreRepositories</literal></link>.</simpara>
|
||
</section>
|
||
<section xml:id="_autoconfiguration_2">
|
||
<title>Autoconfiguration</title>
|
||
<simpara>Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>an instance of <literal>DatastoreTemplate</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>an instance of all user defined repositories extending <literal>CrudRepository</literal>, <literal>PagingAndSortingRepository</literal>, and <literal>DatastoreRepository</literal> (an extension of <literal>PagingAndSortingRepository</literal> with additional Cloud Datastore features) when repositories are enabled</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>an instance of <literal>Datastore</literal> from the Google Cloud Java Client for Datastore, for convenience and lower level API access</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_object_mapping_2">
|
||
<title>Object Mapping</title>
|
||
<simpara>Spring Data Cloud Datastore allows you to map domain POJOs to Cloud Datastore kinds and entities via annotations:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Entity(name = "traders")
|
||
public class Trader {
|
||
|
||
@Id
|
||
@Field(name = "trader_id")
|
||
String traderId;
|
||
|
||
String firstName;
|
||
|
||
String lastName;
|
||
|
||
@Transient
|
||
Double temporaryNumber;
|
||
}</programlisting>
|
||
<simpara>Spring Data Cloud Datastore will ignore any property annotated with <literal>@Transient</literal>.
|
||
These properties will not be written to or read from Cloud Datastore.</simpara>
|
||
<section xml:id="_constructors_2">
|
||
<title>Constructors</title>
|
||
<simpara>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.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Entity(name = "traders")
|
||
public class Trader {
|
||
|
||
@Id
|
||
@Field(name = "trader_id")
|
||
String traderId;
|
||
|
||
String firstName;
|
||
|
||
String lastName;
|
||
|
||
@Transient
|
||
Double temporaryNumber;
|
||
|
||
public Trader(String traderId, String firstName) {
|
||
this.traderId = traderId;
|
||
this.firstName = firstName;
|
||
}
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_kind">
|
||
<title>Kind</title>
|
||
<simpara>The <literal>@Entity</literal> annotation can provide the name of the Cloud Datastore kind that stores instances of the annotated class, one per row.</simpara>
|
||
</section>
|
||
<section xml:id="_keys">
|
||
<title>Keys</title>
|
||
<simpara><literal>@Id</literal> identifies the property corresponding to the ID value.</simpara>
|
||
<simpara>You must annotate one of your POJO’s fields as the ID value, because every entity in Cloud Datastore requires a single ID value:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Entity(name = "trades")
|
||
public class Trade {
|
||
@Id
|
||
@Field(name = "trade_id")
|
||
String tradeId;
|
||
|
||
@Field(name = "trader_id")
|
||
String traderId;
|
||
|
||
String action;
|
||
|
||
Double price;
|
||
|
||
Double shares;
|
||
|
||
String symbol;
|
||
}</programlisting>
|
||
<simpara>Datastore can automatically allocate integer ID values.
|
||
If a POJO instance with a <literal>Long</literal> ID property is written to Cloud Datastore with <literal>null</literal> as the ID value, then Spring Data Cloud Datastore will obtain a newly allocated ID value from Cloud Datastore and set that in the POJO for saving.
|
||
Because primitive <literal>long</literal> ID properties cannot be <literal>null</literal> and default to <literal>0</literal>, keys will not be allocated.</simpara>
|
||
</section>
|
||
<section xml:id="_fields">
|
||
<title>Fields</title>
|
||
<simpara>All accessible properties on POJOs are automatically recognized as a Cloud Datastore field.
|
||
Field naming is generated by the <literal>PropertyNameFieldNamingStrategy</literal> by default defined on the <literal>DatastoreMappingContext</literal> bean.
|
||
The <literal>@Field</literal> annotation optionally provides a different field name than that of the property.</simpara>
|
||
</section>
|
||
<section xml:id="_supported_types_2">
|
||
<title>Supported Types</title>
|
||
<simpara>Spring Data Cloud Datastore supports the following types for regular fields and elements of collections:</simpara>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="2">
|
||
<colspec colname="col_1" colwidth="50*"/>
|
||
<colspec colname="col_2" colwidth="50*"/>
|
||
<thead>
|
||
<row>
|
||
<entry align="left" valign="top">Type</entry>
|
||
<entry align="left" valign="top">Stored as</entry>
|
||
</row>
|
||
</thead>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>com.google.cloud.Timestamp</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.TimestampValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>com.google.cloud.datastore.Blob</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.BlobValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>com.google.cloud.datastore.LatLng</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.LatLngValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>java.lang.Boolean</literal>, <literal>boolean</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.BooleanValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>java.lang.Double</literal>, <literal>double</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.DoubleValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>java.lang.Long</literal>, <literal>long</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.LongValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>java.lang.Integer</literal>, <literal>int</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.LongValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>java.lang.String</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.StringValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>com.google.cloud.datastore.Entity</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.EntityValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>com.google.cloud.datastore.Key</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.KeyValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>byte[]</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.BlobValue</simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara>Java <literal>enum</literal> values</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>com.google.cloud.datastore.StringValue</simpara></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
<simpara>In addition, all types that can be converted to the ones listed in the table by
|
||
<literal>org.springframework.core.convert.support.DefaultConversionService</literal> are supported.</simpara>
|
||
</section>
|
||
<section xml:id="_custom_types_2">
|
||
<title>Custom types</title>
|
||
<simpara>Custom converters can be used extending the type support for user defined types.</simpara>
|
||
<orderedlist numeration="arabic">
|
||
<listitem>
|
||
<simpara>Converters need to implement the <literal>org.springframework.core.convert.converter.Converter</literal> interface in both directions.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>The user defined type needs to be mapped to one of the basic types supported by Cloud Datastore.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>An instance of both Converters (read and write) needs to be passed to the <literal>DatastoreCustomConversions</literal> constructor, which then has to be made available as a <literal>@Bean</literal> for <literal>DatastoreCustomConversions</literal>.</simpara>
|
||
</listitem>
|
||
</orderedlist>
|
||
<simpara>For example:</simpara>
|
||
<simpara>We would like to have a field of type <literal>Album</literal> on our <literal>Singer</literal> POJO and want it to be stored as a string property:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Entity
|
||
public class Singer {
|
||
|
||
@Id
|
||
String singerId;
|
||
|
||
String name;
|
||
|
||
Album album;
|
||
}</programlisting>
|
||
<simpara>Where Album is a simple class:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public class Album {
|
||
String albumName;
|
||
|
||
LocalDate date;
|
||
}</programlisting>
|
||
<simpara>We have to define the two converters:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> //Converter to write custom Album type
|
||
static final Converter<Album, String> ALBUM_STRING_CONVERTER =
|
||
new Converter<Album, String>() {
|
||
@Override
|
||
public String convert(Album album) {
|
||
return album.getAlbumName() + " " + album.getDate().format(DateTimeFormatter.ISO_DATE);
|
||
}
|
||
};
|
||
|
||
//Converters to read custom Album type
|
||
static final Converter<String, Album> STRING_ALBUM_CONVERTER =
|
||
new Converter<String, Album>() {
|
||
@Override
|
||
public Album convert(String s) {
|
||
String[] parts = s.split(" ");
|
||
return new Album(parts[0], LocalDate.parse(parts[parts.length - 1], DateTimeFormatter.ISO_DATE));
|
||
}
|
||
};</programlisting>
|
||
<simpara>That will be configured in our <literal>@Configuration</literal> file:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Configuration
|
||
public class ConverterConfiguration {
|
||
@Bean
|
||
public DatastoreCustomConversions datastoreCustomConversions() {
|
||
return new DatastoreCustomConversions(
|
||
Arrays.asList(
|
||
ALBUM_STRING_CONVERTER,
|
||
STRING_ALBUM_CONVERTER));
|
||
}
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_collections_and_arrays">
|
||
<title>Collections and arrays</title>
|
||
<simpara>Arrays and collections (types that implement <literal>java.util.Collection</literal>) of supported types are supported.
|
||
They are stored as <literal>com.google.cloud.datastore.ListValue</literal>.
|
||
Elements are converted to Cloud Datastore supported types individually. <literal>byte[]</literal> is an exception, it is converted to
|
||
<literal>com.google.cloud.datastore.Blob</literal>.</simpara>
|
||
</section>
|
||
<section xml:id="_custom_converter_for_collections">
|
||
<title>Custom Converter for collections</title>
|
||
<simpara>Users can provide converters from <literal>List<?></literal> to the custom collection type.
|
||
Only read converter is necessary, the Collection API is used on the write side to convert a collection to the internal list type.</simpara>
|
||
<simpara>Collection converters need to implement the <literal>org.springframework.core.convert.converter.Converter</literal> interface.</simpara>
|
||
<simpara>Example:</simpara>
|
||
<simpara>Let’s improve the Singer class from the previous example.
|
||
Instead of a field of type <literal>Album</literal>, we would like to have a field of type <literal>ImmutableSet<Album></literal>:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Entity
|
||
public class Singer {
|
||
|
||
@Id
|
||
String singerId;
|
||
|
||
String name;
|
||
|
||
ImmutableSet<Album> albums;
|
||
}</programlisting>
|
||
<simpara>We have to define a read converter only:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">static final Converter<List<?>, ImmutableSet<?>> LIST_IMMUTABLE_SET_CONVERTER =
|
||
new Converter<List<?>, ImmutableSet<?>>() {
|
||
@Override
|
||
public ImmutableSet<?> convert(List<?> source) {
|
||
return ImmutableSet.copyOf(source);
|
||
}
|
||
};</programlisting>
|
||
<simpara>And add it to the list of custom converters:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Configuration
|
||
public class ConverterConfiguration {
|
||
@Bean
|
||
public DatastoreCustomConversions datastoreCustomConversions() {
|
||
return new DatastoreCustomConversions(
|
||
Arrays.asList(
|
||
LIST_IMMUTABLE_SET_CONVERTER,
|
||
|
||
ALBUM_STRING_CONVERTER,
|
||
STRING_ALBUM_CONVERTER));
|
||
}
|
||
}</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_relationships_2">
|
||
<title>Relationships</title>
|
||
<simpara>There are three ways to represent relationships between entities that are described in this section:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Embedded entities stored directly in the field of the containing entity</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>@Descendant</literal> annotated properties for one-to-many relationships</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>@Reference</literal> annotated properties for general relationships without hierarchy</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_embedded_entities">
|
||
<title>Embedded Entities</title>
|
||
<simpara>Fields whose types are also annotated with <literal>@Entity</literal> are converted to <literal>EntityValue</literal> and stored inside the parent entity.</simpara>
|
||
<simpara>Here is an example of Cloud Datastore entity containing an embedded entity in JSON:</simpara>
|
||
<programlisting language="json" linenumbering="unnumbered">{
|
||
"name" : "Alexander",
|
||
"age" : 47,
|
||
"child" : {"name" : "Philip" }
|
||
}</programlisting>
|
||
<simpara>This corresponds to a simple pair of Java entities:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
|
||
import org.springframework.data.annotation.Id;
|
||
|
||
@Entity("parents")
|
||
public class Parent {
|
||
@Id
|
||
String name;
|
||
|
||
Child child;
|
||
}
|
||
|
||
@Entity
|
||
public class Child {
|
||
String name;
|
||
}</programlisting>
|
||
<simpara><literal>Child</literal> entities are not stored in their own kind.
|
||
They are stored in their entirety in the <literal>child</literal> field of the <literal>parents</literal> kind.</simpara>
|
||
<simpara>Multiple levels of embedded entities are supported.</simpara>
|
||
<note>
|
||
<simpara>Embedded entities don’t need to have <literal>@Id</literal> field, it is only required for top level entities.</simpara>
|
||
</note>
|
||
<simpara>Example:</simpara>
|
||
<simpara>Entities can hold embedded entities that are their own type.
|
||
We can store trees in Cloud Datastore using this feature:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
|
||
import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
|
||
import org.springframework.data.annotation.Id;
|
||
|
||
@Entity
|
||
public class EmbeddableTreeNode {
|
||
@Id
|
||
long value;
|
||
|
||
EmbeddableTreeNode left;
|
||
|
||
EmbeddableTreeNode right;
|
||
|
||
Map<String, Long> longValues;
|
||
|
||
Map<String, List<Timestamp>> listTimestamps;
|
||
|
||
public EmbeddableTreeNode(long value, EmbeddableTreeNode left, EmbeddableTreeNode right) {
|
||
this.value = value;
|
||
this.left = left;
|
||
this.right = right;
|
||
}
|
||
}</programlisting>
|
||
<section xml:id="_maps">
|
||
<title>Maps</title>
|
||
<simpara>Maps will be stored as embedded entities where the key values become the field names in the embedded entity.
|
||
The value types in these maps can be any regularly supported property type, and the key values will be converted to String using the configured converters.</simpara>
|
||
<simpara>Also, a collection of entities can be embedded; it will be converted to <literal>ListValue</literal> on write.</simpara>
|
||
<simpara>Example:</simpara>
|
||
<simpara>Instead of a binary tree from the previous example, we would like to store a general tree
|
||
(each node can have an arbitrary number of children) in Cloud Datastore.
|
||
To do that, we need to create a field of type <literal>List<EmbeddableTreeNode></literal>:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
|
||
import org.springframework.data.annotation.Id;
|
||
|
||
public class EmbeddableTreeNode {
|
||
@Id
|
||
long value;
|
||
|
||
List<EmbeddableTreeNode> children;
|
||
|
||
Map<String, EmbeddableTreeNode> siblingNodes;
|
||
|
||
Map<String, Set<EmbeddableTreeNode>> subNodeGroups;
|
||
|
||
public EmbeddableTreeNode(List<EmbeddableTreeNode> children) {
|
||
this.children = children;
|
||
}
|
||
}</programlisting>
|
||
<simpara>Because Maps are stored as entities, they can further hold embedded entities:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Singular embedded objects in the value can be stored in the values of embedded Maps.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Collections of embedded objects in the value can also be stored as the values of embedded Maps.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Maps in the value are further stored as embedded entities with the same rules applied recursively for their values.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_ancestor_descendant_relationships">
|
||
<title>Ancestor-Descendant Relationships</title>
|
||
<simpara>Parent-child relationships are supported via the <literal>@Descendants</literal> annotation.</simpara>
|
||
<simpara>Unlike embedded children, descendants are fully-formed entities residing in their own kinds.
|
||
The parent entity does not have an extra field to hold the descendant entities.
|
||
Instead, the relationship is captured in the descendants' keys, which refer to their parent entities:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Descendants;
|
||
import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
|
||
import org.springframework.data.annotation.Id;
|
||
|
||
@Entity("orders")
|
||
public class ShoppingOrder {
|
||
@Id
|
||
long id;
|
||
|
||
@Descendants
|
||
List<Item> items;
|
||
}
|
||
|
||
@Entity("purchased_item")
|
||
public class Item {
|
||
@Id
|
||
Key purchasedItemKey;
|
||
|
||
String name;
|
||
|
||
Timestamp timeAddedToOrder;
|
||
}</programlisting>
|
||
<simpara>For example, an instance of a GQL key-literal representation for <literal>Item</literal> would also contain the parent <literal>ShoppingOrder</literal> ID value:</simpara>
|
||
<screen>Key(orders, '12345', purchased_item, 'eggs')</screen>
|
||
<simpara>The GQL key-literal representation for the parent <literal>ShoppingOrder</literal> would be:</simpara>
|
||
<screen>Key(orders, '12345')</screen>
|
||
<simpara>The Cloud Datastore entities exist separately in their own kinds.</simpara>
|
||
<simpara>The <literal>ShoppingOrder</literal>:</simpara>
|
||
<screen>{
|
||
"id" : 12345
|
||
}</screen>
|
||
<simpara>The two items inside that order:</simpara>
|
||
<screen>{
|
||
"purchasedItemKey" : Key(orders, '12345', purchased_item, 'eggs'),
|
||
"name" : "eggs",
|
||
"timeAddedToOrder" : "2014-09-27 12:30:00.45-8:00"
|
||
}
|
||
|
||
{
|
||
"purchasedItemKey" : Key(orders, '12345', purchased_item, 'sausage'),
|
||
"name" : "sausage",
|
||
"timeAddedToOrder" : "2014-09-28 11:30:00.45-9:00"
|
||
}</screen>
|
||
<simpara>The parent-child relationship structure of objects is stored in Cloud Datastore using Datastore’s <link xl:href="https://cloud.google.com/datastore/docs/concepts/entities#ancestor_paths">ancestor relationships</link>.
|
||
Because the relationships are defined by the Ancestor mechanism, there is no extra column needed in either the parent or child entity to store this relationship.
|
||
The relationship link is part of the descendant entity’s key value.
|
||
These relationships can be many levels deep.</simpara>
|
||
<simpara>Properties holding child entities must be collection-like, but they can be any of the supported inter-convertible collection-like types that are supported for regular properties such as <literal>List</literal>, arrays, <literal>Set</literal>, etc…​
|
||
Child items must have <literal>Key</literal> as their ID type because Cloud Datastore stores the ancestor relationship link inside the keys of the children.</simpara>
|
||
<simpara>Reading or saving an entity automatically causes all subsequent levels of children under that entity to be read or saved, respectively.
|
||
If a new child is created and added to a property annotated <literal>@Descendants</literal> and the key property is left null, then a new key will be allocated for that child.
|
||
The ordering of the retrieved children may not be the same as the ordering in the original property that was saved.</simpara>
|
||
<simpara>Child entities cannot be moved from the property of one parent to that of another unless the child’s key property is set to <literal>null</literal> or a value that contains the new parent as an ancestor.
|
||
Since Cloud Datastore entity keys can have multiple parents, it is possible that a child entity appears in the property of multiple parent entities.
|
||
Because entity keys are immutable in Cloud Datastore, to change the key of a child you must delete the existing one and re-save it with the new key.</simpara>
|
||
</section>
|
||
<section xml:id="_key_reference_relationships">
|
||
<title>Key Reference Relationships</title>
|
||
<simpara>General relationships can be stored using the <literal>@Reference</literal> annotation.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Reference;
|
||
import org.springframework.data.annotation.Id;
|
||
|
||
@Entity
|
||
public class ShoppingOrder {
|
||
@Id
|
||
long id;
|
||
|
||
@Reference
|
||
List<Item> items;
|
||
|
||
@Reference
|
||
Item specialSingleItem;
|
||
}
|
||
|
||
@Entity
|
||
public class Item {
|
||
@Id
|
||
Key purchasedItemKey;
|
||
|
||
String name;
|
||
|
||
Timestamp timeAddedToOrder;
|
||
}</programlisting>
|
||
<simpara><literal>@Reference</literal> relationships are between fully-formed entities residing in their own kinds.
|
||
The relationship between <literal>ShoppingOrder</literal> and <literal>Item</literal> entities are stored as a Key field inside <literal>ShoppingOrder</literal>, which are resolved to the underlying Java entity type by Spring Data Cloud Datastore:</simpara>
|
||
<screen>{
|
||
"id" : 12345,
|
||
"specialSingleItem" : Key(item, "milk"),
|
||
"items" : [ Key(item, "eggs"), Key(item, "sausage") ]
|
||
}</screen>
|
||
<simpara>Reference properties can either be singular or collection-like.
|
||
These properties correspond to actual columns in the entity and Cloud Datastore Kind that hold the key values of the referenced entities.
|
||
The referenced entities are full-fledged entities of other Kinds.</simpara>
|
||
<simpara>Similar to the <literal>@Descendants</literal> relationships, reading or writing an entity will recursively read or write all of the referenced entities at all levels.
|
||
If referenced entities have <literal>null</literal> ID values, then they will be saved as new entities and will have ID values allocated by Cloud Datastore.
|
||
There are no requirements for relationships between the key of an entity and the keys that entity holds as references.
|
||
The order of collection-like reference properties is not preserved when reading back from Cloud Datastore.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_datastore_operations_template">
|
||
<title>Datastore Operations & Template</title>
|
||
<simpara><literal>DatastoreOperations</literal> and its implementation, <literal>DatastoreTemplate</literal>, provides the Template pattern familiar to Spring developers.</simpara>
|
||
<simpara>Using the auto-configuration provided by Spring Boot Starter for Datastore, your Spring application context will contain a fully configured <literal>DatastoreTemplate</literal> object that you can autowire in your application:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@SpringBootApplication
|
||
public class DatastoreTemplateExample {
|
||
|
||
@Autowired
|
||
DatastoreTemplate datastoreTemplate;
|
||
|
||
public void doSomething() {
|
||
this.datastoreTemplate.deleteAll(Trader.class);
|
||
//...
|
||
Trader t = new Trader();
|
||
//...
|
||
this.datastoreTemplate.save(t);
|
||
//...
|
||
List<Trader> traders = datastoreTemplate.findAll(Trader.class);
|
||
//...
|
||
}
|
||
}</programlisting>
|
||
<simpara>The Template API provides convenience methods for:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Write operations (saving and deleting)</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Read-write transactions</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_gql_query">
|
||
<title>GQL Query</title>
|
||
<simpara>In addition to retrieving entities by their IDs, you can also submit queries.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> <T> Iterable<T> query(Query<? extends BaseEntity> query, Class<T> entityClass);
|
||
|
||
<A, T> Iterable<T> query(Query<A> query, Function<A, T> entityFunc);
|
||
|
||
Iterable<Key> queryKeys(Query<Key> query);</programlisting>
|
||
<simpara>These methods, respectively, allow querying for:
|
||
* entities mapped by a given entity class using all the same mapping and converting features
|
||
* arbitrary types produced by a given mapping function
|
||
* only the Cloud Datastore keys of the entities found by the query</simpara>
|
||
</section>
|
||
<section xml:id="_find_by_ids">
|
||
<title>Find by ID(s)</title>
|
||
<simpara>Datstore reading a single entity or multiple entities in a kind.</simpara>
|
||
<simpara>Using <literal>DatastoreTemplate</literal> you can execute reads, for example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">Trader trader = this.datastoreTemplate.findById("trader1", Trader.class);
|
||
|
||
List<Trader> traders = this.datastoreTemplate.findAllById(ImmutableList.of("trader1", "trader2"), Trader.class);
|
||
|
||
List<Trader> allTraders = this.datastoreTemplate.findAll(Trader.class);</programlisting>
|
||
<simpara>Cloud Datastore executes key-based reads with strong consistency, but queries with eventual consistency.
|
||
In the example above the first two reads utilize keys, while the third is executed using a query based on the corresponding Kind of <literal>Trader</literal>.</simpara>
|
||
<section xml:id="_indexes">
|
||
<title>Indexes</title>
|
||
<simpara>By default, all fields are indexed.
|
||
To disable indexing on a particular field, <literal>@Unindexed</literal> annotation can be used.</simpara>
|
||
<simpara>Example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">import org.springframework.cloud.gcp.data.datastore.core.mapping.Unindexed;
|
||
|
||
public class ExampleItem {
|
||
long indexedField;
|
||
|
||
@Unindexed
|
||
long unindexedField;
|
||
}</programlisting>
|
||
<simpara>When using queries directly or via Query Methods, Cloud Datastore requires <link xl:href="https://cloud.google.com/datastore/docs/concepts/indexes">composite custom indexes</link> if the select statement is not <literal>SELECT *</literal> or if there is more than one filtering condition in the <literal>WHERE</literal> clause.</simpara>
|
||
</section>
|
||
<section xml:id="_read_with_offsets_limits_and_sorting">
|
||
<title>Read with offsets, limits, and sorting</title>
|
||
<simpara><literal>DatastoreRepository</literal> and custom-defined entity repositories implement the Spring Data <literal>PagingAndSortingRepository</literal>, which supports offsets and limits using page numbers and page sizes.
|
||
Paging and sorting options are also supported in <literal>DatastoreTemplate</literal> by supplying a <literal>DatastoreQueryOptions</literal> to <literal>findAll</literal>.</simpara>
|
||
</section>
|
||
<section xml:id="_partial_read_2">
|
||
<title>Partial read</title>
|
||
<simpara>This feature is not supported yet.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_write_update_2">
|
||
<title>Write / Update</title>
|
||
<simpara>The write methods of <literal>DatastoreOperations</literal> accept a POJO and writes all of its properties to Datastore.
|
||
The required Datastore kind and entity metadata is obtained from the given object’s actual type.</simpara>
|
||
<simpara>If a POJO was retrieved from Datastore and its ID value was changed and then written or updated, the operation will occur as if against a row with the new ID value.
|
||
The entity with the original ID value will not be affected.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">Trader t = new Trader();
|
||
this.datastoreTemplate.save(t);</programlisting>
|
||
<simpara>The <literal>save</literal> method behaves as update-or-insert.</simpara>
|
||
<section xml:id="_partial_update_2">
|
||
<title>Partial Update</title>
|
||
<simpara>This feature is not supported yet.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_transactions_2">
|
||
<title>Transactions</title>
|
||
<simpara>Read and write transactions are provided by <literal>DatastoreOperations</literal> via the <literal>performTransaction</literal> method:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
DatastoreOperations myDatastoreOperations;
|
||
|
||
public String doWorkInsideTransaction() {
|
||
return myDatastoreOperations.performTransaction(
|
||
transactionDatastoreOperations -> {
|
||
// Work with transactionDatastoreOperations here.
|
||
// It is also a DatastoreOperations object.
|
||
|
||
return "transaction completed";
|
||
}
|
||
);
|
||
}</programlisting>
|
||
<simpara>The <literal>performTransaction</literal> method accepts a <literal>Function</literal> that is provided an instance of a <literal>DatastoreOperations</literal> 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 <literal>DatastoreOperations</literal> with an exception:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>It cannot perform sub-transactions.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Because of Cloud Datastore’s consistency guarantees, there are <link xl:href="https://cloud.google.com/datastore/docs/concepts/transactions#what_can_be_done_in_a_transaction">limitations</link> to the operations and relationships among entities used inside transactions.</simpara>
|
||
<section xml:id="_declarative_transactions_with_transactional_annotation_2">
|
||
<title>Declarative Transactions with @Transactional Annotation</title>
|
||
<simpara>This feature requires a bean of <literal>DatastoreTransactionManager</literal>, which is provided when using <literal>spring-cloud-gcp-starter-data-datastore</literal>.</simpara>
|
||
<simpara><literal>DatastoreTemplate</literal> and <literal>DatastoreRepository</literal> support running methods with the <literal>@Transactional</literal> <link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative">annotation</link> as transactions.
|
||
If a method annotated with <literal>@Transactional</literal> calls another method also annotated, then both methods will work within the same transaction.
|
||
<literal>performTransaction</literal> cannot be used in <literal>@Transactional</literal> annotated methods because Cloud Datastore does not support transactions within transactions.</simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_read_write_support_for_maps">
|
||
<title>Read-Write Support for Maps</title>
|
||
<simpara>You can work with Maps of type <literal>Map<String, ?></literal> instead of with entity objects by directly reading and writing them to and from Cloud Datastore.</simpara>
|
||
<note>
|
||
<simpara>This is a different situation than using entity objects that contain Map properties.</simpara>
|
||
</note>
|
||
<simpara>The map keys are used as field names for a Datastore entity and map values are converted to Datastore supported types.
|
||
Only simple types are supported (i.e. collections are not supported).
|
||
Converters for custom value types can be added (see <xref linkend="_custom_types"/> section).</simpara>
|
||
<simpara>Example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">Map<String, Long> map = new HashMap<>();
|
||
map.put("field1", 1L);
|
||
map.put("field2", 2L);
|
||
map.put("field3", 3L);
|
||
|
||
keyForMap = datastoreTemplate.createKey("kindName", "id");
|
||
|
||
//write a map
|
||
datastoreTemplate.writeMap(keyForMap, map);
|
||
|
||
//read a map
|
||
Map<String, Long> loadedMap = datastoreTemplate.findByIdAsMap(keyForMap, Long.class);</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_repositories_2">
|
||
<title>Repositories</title>
|
||
<simpara><link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories">Spring Data Repositories</link> are an abstraction that can reduce boilerplate code.</simpara>
|
||
<simpara>For example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TraderRepository extends DatastoreRepository<Trader, String> {
|
||
}</programlisting>
|
||
<simpara>Spring Data generates a working implementation of the specified interface, which can be autowired into an application.</simpara>
|
||
<simpara>The <literal>Trader</literal> type parameter to <literal>DatastoreRepository</literal> refers to the underlying domain type.
|
||
The second type parameter, <literal>String</literal> in this case, refers to the type of the key of the domain type.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public class MyApplication {
|
||
|
||
@Autowired
|
||
TraderRepository traderRepository;
|
||
|
||
public void demo() {
|
||
|
||
this.traderRepository.deleteAll();
|
||
String traderId = "demo_trader";
|
||
Trader t = new Trader();
|
||
t.traderId = traderId;
|
||
this.tradeRepository.save(t);
|
||
|
||
Iterable<Trader> allTraders = this.traderRepository.findAll();
|
||
|
||
int count = this.traderRepository.count();
|
||
}
|
||
}</programlisting>
|
||
<simpara>Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving, counting, and deleting based on filtering and paging parameters.
|
||
Filtering parameters can be of types supported by your configured custom converters.</simpara>
|
||
<section xml:id="_query_methods_by_convention_2">
|
||
<title>Query methods by convention</title>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
|
||
List<Trader> findByAction(String action);
|
||
|
||
int countByAction(String action);
|
||
|
||
boolean existsByAction(String action);
|
||
|
||
List<Trade> findTop3ByActionAndSymbolAndPriceGreaterThanAndPriceLessThanOrEqualOrderBySymbolDesc(
|
||
String action, String symbol, double priceFloor, double priceCeiling);
|
||
|
||
Page<TestEntity> findByAction(String action, Pageable pageable);
|
||
|
||
Slice<TestEntity> findBySymbol(String symbol, Pageable pageable);
|
||
|
||
List<TestEntity> findBySymbol(String symbol, Sort sort);
|
||
}</programlisting>
|
||
<simpara>In the example above the <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories.query-methods">query methods</link> in <literal>TradeRepository</literal> are generated based on the name of the methods using thehttps://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation[Spring Data Query creation naming convention].</simpara>
|
||
<simpara>Cloud Datastore only supports filter components joined by AND, and the following operations:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>equals</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>greater than or equals</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>greater than</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>less than or equals</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>less than</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>is null</literal></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository.
|
||
Because of Cloud Datastore’s requirement that explicitly selected fields must all appear in a composite index together, <literal>find</literal> name-based query methods are run as <literal>SELECT *</literal>.</simpara>
|
||
<simpara>Delete queries are also supported.
|
||
For example, query methods such as <literal>deleteByAction</literal> or <literal>removeByAction</literal> delete entities found by <literal>findByAction</literal>.
|
||
Delete queries are executed as separate read and delete operations instead of as a single transaction because Cloud Datastore cannot query in transactions unless ancestors for queries are specified.
|
||
As a result, <literal>removeBy</literal> and <literal>deleteBy</literal> name-convention query methods cannot be used inside transactions via either <literal>performInTransaction</literal> or <literal>@Transactional</literal> annotation.</simpara>
|
||
<simpara>Delete queries can have the following return types:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>An integer type that is the number of entities deleted</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>A collection of entities that were deleted</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>'void'</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Methods can have <literal>org.springframework.data.domain.Pageable</literal> parameter to control pagination and sorting, or <literal>org.springframework.data.domain.Sort</literal> parameter to control sorting only.
|
||
See <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories.query-methods">Spring Data documentation</link> for details.</simpara>
|
||
<simpara>For returning multiple items in a repository method, we support Java collections as well as <literal>org.springframework.data.domain.Page</literal> and <literal>org.springframework.data.domain.Slice</literal>.
|
||
If a method’s return type is <literal>org.springframework.data.domain.Page</literal>, the returned object will include current page, total number of results and total number of pages.</simpara>
|
||
<note>
|
||
<simpara>Methods that return <literal>Page</literal> execute an additional query to compute total number of pages.
|
||
Methods that return <literal>Slice</literal>, on the other hand, don’t execute any additional queries and therefore are much more efficient.</simpara>
|
||
</note>
|
||
</section>
|
||
<section xml:id="_custom_gql_query_methods">
|
||
<title>Custom GQL query methods</title>
|
||
<simpara>Custom GQL queries can be mapped to repository methods in one of two ways:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>namedQueries</literal> properties file</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>using the <literal>@Query</literal> annotation</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<section xml:id="_query_methods_with_annotation_2">
|
||
<title>Query methods with annotation</title>
|
||
<simpara>Using the <literal>@Query</literal> annotation:</simpara>
|
||
<simpara>The names of the tags of the GQL correspond to the <literal>@Param</literal> annotated names of the method parameters.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TraderRepository extends DatastoreRepository<Trader, String> {
|
||
|
||
@Query("SELECT * FROM traders WHERE name = @trader_name")
|
||
List<Trader> tradersByName(@Param("trader_name") String traderName);
|
||
|
||
@Query("SELECT * FROM test_entities_ci WHERE id = @id_val")
|
||
TestEntity getOneTestEntity(@Param("id_val") long id);
|
||
}</programlisting>
|
||
<simpara>The following parameter types are supported:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.Timestamp</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.datastore.Blob</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.datastore.Key</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>com.google.cloud.datastore.Cursor</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Boolean</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Double</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.Long</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>java.lang.String</literal></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>enum</literal> values.
|
||
These are queried as <literal>String</literal> values.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>With the exception of <literal>Cursor</literal>, array forms of each of the types are also supported.</simpara>
|
||
<simpara>If you would like to obtain the count of items of a query or if there are any items returned by the query, set the <literal>count = true</literal> or <literal>exists = true</literal> properties of the <literal>@Query</literal> annotation, respectively.
|
||
The return type of the query method in these cases should be an integer type or a boolean type.</simpara>
|
||
<simpara>Cloud Datastore provides provides the <literal>SELECT <emphasis>key</emphasis> FROM …​</literal> special column for all kinds that retrieves the <literal>Key`s of each row.
|
||
Selecting this special `<emphasis>key</emphasis></literal> column is especially useful and efficient for <literal>count</literal> and <literal>exists</literal> queries.</simpara>
|
||
<simpara>You can also query for non-entity types:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered"> @Query(value = "SELECT __key__ from test_entities_ci")
|
||
List<Key> getKeys();
|
||
|
||
@Query(value = "SELECT __key__ from test_entities_ci limit 1")
|
||
Key getKey();
|
||
|
||
@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val")
|
||
List<String> getIds(@Param("id_val") long id);
|
||
|
||
@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val limit 1")
|
||
String getOneId(@Param("id_val") long id);</programlisting>
|
||
<simpara>SpEL can be used to provide GQL parameters:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act
|
||
AND price > :#{#priceRadius * -1} AND price < :#{#priceRadius * 2}")
|
||
List<Trade> fetchByActionNamedQuery(@Param("act") String action, @Param("priceRadius") Double r);</programlisting>
|
||
<simpara>Kind names can be directly written in the GQL annotations.
|
||
Kind names can also be resolved from the <literal>@Entity</literal> annotation on domain classes.</simpara>
|
||
<simpara>In this case, the query should refer to table names with fully qualified class names surrounded by <literal>|</literal> characters: <literal>|fully.qualified.ClassName|</literal>.
|
||
This is useful when SpEL expressions appear in the kind name provided to the <literal>@Entity</literal> annotation.
|
||
For example:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act")
|
||
List<Trade> fetchByActionNamedQuery(@Param("act") String action);</programlisting>
|
||
</section>
|
||
<section xml:id="_query_methods_with_named_queries_properties_2">
|
||
<title>Query methods with named queries properties</title>
|
||
<simpara>You can also specify queries with Cloud Datastore parameter tags and SpEL expressions in properties files.</simpara>
|
||
<simpara>By default, the <literal>namedQueriesLocation</literal> attribute on <literal>@EnableDatastoreRepositories</literal> points to the <literal>META-INF/datastore-named-queries.properties</literal> file.
|
||
You can specify the query for a method in the properties file by providing the GQL as the value for the "interface.method" property:</simpara>
|
||
<programlisting language="properties" linenumbering="unnumbered">Trader.fetchByName=SELECT * FROM traders WHERE name = @tag0</programlisting>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TraderRepository extends DatastoreRepository<Trader, String> {
|
||
|
||
// This method uses the query from the properties file instead of one generated based on name.
|
||
List<Trader> fetchByName(@Param("tag0") String traderName);
|
||
|
||
}</programlisting>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_transactions_3">
|
||
<title>Transactions</title>
|
||
<simpara>These transactions work very similarly to those of <literal>DatastoreOperations</literal>, but is specific to the repository’s domain type and provides repository functions instead of template functions.</simpara>
|
||
<simpara>For example, this is a read-write transaction:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
DatastoreRepository myRepo;
|
||
|
||
public String doWorkInsideTransaction() {
|
||
return myRepo.performTransaction(
|
||
transactionDatastoreRepo -> {
|
||
// Work with the single-transaction transactionDatastoreRepo here.
|
||
// This is a DatastoreRepository object.
|
||
|
||
return "transaction completed";
|
||
}
|
||
);
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_projections_2">
|
||
<title>Projections</title>
|
||
<simpara>Spring Data Cloud Datastore supports <link xl:href="https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#projections">projections</link>.
|
||
You can define projection interfaces based on domain types and add query methods that return them in your repository:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">public interface TradeProjection {
|
||
|
||
String getAction();
|
||
|
||
@Value("#{target.symbol + ' ' + target.action}")
|
||
String getSymbolAndAction();
|
||
}
|
||
|
||
public interface TradeRepository extends DatastoreRepository<Trade, Key> {
|
||
|
||
List<Trade> findByTraderId(String traderId);
|
||
|
||
List<TradeProjection> findByAction(String action);
|
||
|
||
@Query("SELECT action, symbol FROM trades WHERE action = @action")
|
||
List<TradeProjection> findByQuery(String action);
|
||
}</programlisting>
|
||
<simpara>Projections can be provided by name-convention-based query methods as well as by custom GQL queries.
|
||
If using custom GQL queries, you can further restrict the fields retrieved from Cloud Datastore to just those required by the projection.
|
||
However, custom select statements (those not using <literal>SELECT *</literal>) require composite indexes containing the selected fields.</simpara>
|
||
<simpara>Properties of projection types defined using SpEL use the fixed name <literal>target</literal> for the underlying domain object.
|
||
As a result, accessing underlying properties take the form <literal>target.<property-name></literal>.</simpara>
|
||
</section>
|
||
<section xml:id="_rest_repositories_2">
|
||
<title>REST Repositories</title>
|
||
<simpara>When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-data-rest</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>If you prefer to configure parameters (such as path), you can use <literal>@RepositoryRestResource</literal> annotation:</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
|
||
public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
|
||
}</programlisting>
|
||
<simpara>For example, you can retrieve all <literal>Trade</literal> objects in the repository by using <literal>curl http://<server>:<port>/trades</literal>, or any specific trade via <literal>curl http://<server>:<port>/trades/<trader_id></literal>.</simpara>
|
||
<simpara>You can also write trades using <literal>curl -XPOST -H"Content-Type: application/json" -<link xl:href="mailto:d@test.json">d@test.json</link> http://<server>:<port>/trades/</literal> where the file <literal>test.json</literal> holds the JSON representation of a <literal>Trade</literal> object.</simpara>
|
||
<simpara>To delete trades, you can use <literal>curl -XDELETE http://<server>:<port>/trades/<trader_id></literal></simpara>
|
||
</section>
|
||
</section>
|
||
<section xml:id="_sample_10">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-datastore-basic-sample">Simple Spring Boot Application</link> and more advanced <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-data-datastore-sample">Sample Spring Boot Application</link> are provided to show how to use the Spring Data Cloud Datastore starter and template.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_cloud_memorystore_for_redis">
|
||
<title>Cloud Memorystore for Redis</title>
|
||
<section xml:id="_spring_caching">
|
||
<title>Spring Caching</title>
|
||
<simpara><link xl:href="https://cloud.google.com/memorystore/">Cloud Memorystore for Redis</link> provides a fully managed in-memory data store service.
|
||
Cloud Memorystore is compatible with the Redis protocol, allowing easy integration with <link xl:href="https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html">Spring Caching</link>.</simpara>
|
||
<simpara>All you have to do is create a Cloud Memorystore instance and use its IP address in <literal>application.properties</literal> file as <literal>spring.redis.host</literal> property value.
|
||
Everything else is exactly the same as setting up redis-backed Spring caching.</simpara>
|
||
<note>
|
||
<simpara>Memorystore instances and your application instances have to be located in the same region.</simpara>
|
||
</note>
|
||
<simpara>In short, the following dependencies are needed:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-cache</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>And then you can use <literal>org.springframework.cache.annotation.Cacheable</literal> annotation for methods you’d like to be cached.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Cacheable("cache1")
|
||
public String hello(@PathVariable String name) {
|
||
....
|
||
}</programlisting>
|
||
<simpara>If you are interested in a detailed how-to guide, please check <link xl:href="https://codelabs.developers.google.com/codelabs/cloud-spring-cache-memorystore/">Spring Boot Caching using Cloud Memorystore codelab</link>.</simpara>
|
||
<simpara>Cloud Memorystore documentation can be found <link xl:href="https://cloud.google.com/memorystore/docs/redis/">here</link>.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_cloud_identity_aware_proxy_iap_authentication">
|
||
<title>Cloud Identity-Aware Proxy (IAP) Authentication</title>
|
||
<simpara><link xl:href="https://cloud.google.com/iap/">Cloud Identity-Aware Proxy (IAP)</link> provides a security layer over applications deployed to Google Cloud.</simpara>
|
||
<simpara>The IAP starter uses <link xl:href="https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2resourceserver">Spring Security OAuth 2.0 Resource Server</link> functionality to automatically extract user identity from the proxy-injected <literal>x-goog-iap-jwt-assertion</literal> HTTP header.</simpara>
|
||
<simpara>The following claims are validated automatically:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Issue time</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Expiration time</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Issuer</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Audience</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>The audience (<literal>"aud"</literal>) validation is automatically configured when the application is running on App Engine Standard or App Engine Flexible.
|
||
For other runtime environments, a custom audience must be provided through <literal>spring.cloud.gcp.security.iap.audience</literal> property.
|
||
The custom property, if specified, overrides the automatic App Engine audience detection.</simpara>
|
||
<important>
|
||
<simpara>There is no automatic audience string configuration for Compute Engine or Kubernetes Engine.
|
||
To use the IAP starter on GCE/GKE, find the Audience string per instructions in the <link xl:href="https://cloud.google.com/iap/docs/signed-headers-howto#verify_the_jwt_payload">Verify the JWT payload</link> guide, and specify it in the <literal>spring.cloud.gcp.security.iap.audience</literal> property.
|
||
Otherwise, the application will fail to start with <literal>No qualifying bean of type 'org.springframework.cloud.gcp.security.iap.AudienceProvider' available</literal> message.</simpara>
|
||
</important>
|
||
<note>
|
||
<simpara>If you create a custom <link xl:href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html"><literal>WebSecurityConfigurerAdapter</literal></link>, enable extracting user identity by adding <literal>.oauth2ResourceServer().jwt()</literal> configuration to the <link xl:href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html"><literal>HttpSecurity</literal></link> object.
|
||
If no custom <link xl:href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html"><literal>WebSecurityConfigurerAdapter</literal></link> is present, nothing needs to be done because Spring Boot will add this customization by default.</simpara>
|
||
</note>
|
||
<simpara>Starter Maven coordinates, using <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/blob/master/spring-cloud-gcp-dependencies/pom.xml">Spring Cloud GCP BOM</link>:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-security-iap</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Starter Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-security-iap'
|
||
}</screen>
|
||
<section xml:id="_configuration_6">
|
||
<title>Configuration</title>
|
||
<simpara>The following properties are available.</simpara>
|
||
<caution>
|
||
<simpara>Modifying registry, algorithm, and header properties might be useful for testing, but the defaults should not be changed in production.</simpara>
|
||
</caution>
|
||
<informaltable frame="all" rowsep="1" colsep="1">
|
||
<tgroup cols="4">
|
||
<colspec colname="col_1" colwidth="25*"/>
|
||
<colspec colname="col_2" colwidth="25*"/>
|
||
<colspec colname="col_3" colwidth="25*"/>
|
||
<colspec colname="col_4" colwidth="25*"/>
|
||
<thead>
|
||
<row>
|
||
<entry align="left" valign="top">Name</entry>
|
||
<entry align="left" valign="top">Description</entry>
|
||
<entry align="left" valign="top">Required</entry>
|
||
<entry align="left" valign="top">Default</entry>
|
||
</row>
|
||
</thead>
|
||
<tbody>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.security.iap.registry</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Link to JWK public key registry.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>true</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal><link xl:href="https://www.gstatic.com/iap/verify/public_key-jwk">https://www.gstatic.com/iap/verify/public_key-jwk</link></literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.security.iap.algorithm</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Encryption algorithm used to sign the JWK token.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>true</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>ES256</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.security.iap.header</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Header from which to extract the JWK key.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>true</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal>x-goog-iap-jwt-assertion</literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.security.iap.issuer</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>JWK issuer to verify.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>true</simpara></entry>
|
||
<entry align="left" valign="top"><simpara><literal><link xl:href="https://cloud.google.com/iap">https://cloud.google.com/iap</link></literal></simpara></entry>
|
||
</row>
|
||
<row>
|
||
<entry align="left" valign="top"><simpara><literal>spring.cloud.gcp.security.iap.audience</literal></simpara></entry>
|
||
<entry align="left" valign="top"><simpara>Custom JWK audience to verify.</simpara></entry>
|
||
<entry align="left" valign="top"><simpara>false on App Engine; true on GCE/GKE</simpara></entry>
|
||
<entry align="left" valign="top"></entry>
|
||
</row>
|
||
</tbody>
|
||
</tgroup>
|
||
</informaltable>
|
||
</section>
|
||
<section xml:id="_sample_11">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-security-iap-sample">sample application</link> is available.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_google_cloud_vision">
|
||
<title>Google Cloud Vision</title>
|
||
<simpara>The <link xl:href="https://cloud.google.com/vision/">Google Cloud Vision API</link> allows users to leverage machine learning algorithms for processing images including: image classification, face detection, text extraction, and others.</simpara>
|
||
<simpara>Spring Cloud GCP provides:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>A convenience starter which automatically configures authentication settings and client objects needed to begin using the <link xl:href="https://cloud.google.com/vision/">Google Cloud Vision API</link>.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>A Cloud Vision Template which simplifies interactions with the Cloud Vision API.</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara>Allows you to easily send images to the API as Spring Resources.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara>Offers convenience methods for common operations, such as extracting the text from an image.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Maven coordinates, using Spring Cloud GCP BOM:</simpara>
|
||
<programlisting language="xml" linenumbering="unnumbered"><dependency>
|
||
<groupId>org.springframework.cloud</groupId>
|
||
<artifactId>spring-cloud-gcp-starter-vision</artifactId>
|
||
</dependency></programlisting>
|
||
<simpara>Gradle coordinates:</simpara>
|
||
<screen>dependencies {
|
||
compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-vision'
|
||
}</screen>
|
||
<section xml:id="_cloud_vision_template">
|
||
<title>Cloud Vision Template</title>
|
||
<simpara>The <literal>CloudVisionTemplate</literal> offers a simple way to use the Cloud Vision APIs with Spring Resources.</simpara>
|
||
<simpara>After you add the <literal>spring-cloud-gcp-starter-vision</literal> dependency to your project, you may <literal>@Autowire</literal> an instance of <literal>CloudVisionTemplate</literal> to use in your code.</simpara>
|
||
<simpara>The <literal>CloudVisionTemplate</literal> offers the following method for interfacing with Cloud Vision:</simpara>
|
||
<simpara><literal>public AnnotateImageResponse analyzeImage(Resource imageResource, Feature.Type…​ featureTypes)</literal></simpara>
|
||
<simpara><emphasis role="strong">Parameters:</emphasis></simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><literal>Resource imageResource</literal> refers to the Spring Resource of the image object you wish to analyze.
|
||
The Google Cloud Vision documentation provides a <link xl:href="https://cloud.google.com/vision/docs/supported-files">list of the image types that they support</link>.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><literal>Feature.Type…​ featureTypes</literal> refers to a var-arg array of Cloud Vision Features to extract from the image.
|
||
A feature refers to a kind of image analysis one wishes to perform on an image, such as label detection, OCR recognition, facial detection, etc.
|
||
One may specify multiple features to analyze within one request.
|
||
A full list of Cloud Vision Features is provided in the <link xl:href="https://cloud.google.com/vision/docs/features">Cloud Vision Feature docs</link>.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara><emphasis role="strong">Returns:</emphasis></simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#google.cloud.vision.v1.AnnotateImageResponse"><literal>AnnotateImageResponse</literal></link> contains the results of all the feature analyses that were specified in the request.
|
||
For each feature type that you provide in the request, <literal>AnnotateImageResponse</literal> provides a getter method to get the result of that feature analysis.
|
||
For example, if you analyzed an image using the <literal>LABEL_DETECTION</literal> feature, you would retrieve the results from the response using <literal>annotateImageResponse.getLabelAnnotationsList()</literal>.</simpara>
|
||
<simpara><literal>AnnotateImageResponse</literal> is provided by the Google Cloud Vision libraries; please consult the <link xl:href="https://cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#google.cloud.vision.v1.AnnotateImageResponse">RPC reference</link> or <link xl:href="http://googleapis.github.io/googleapis/java/all/latest/apidocs/com/google/cloud/vision/v1/AnnotateImageResponse.html">Javadoc</link> for more details.
|
||
Additionally, you may consult the <link xl:href="https://cloud.google.com/vision/docs/">Cloud Vision docs</link> to familiarize yourself with the concepts and features of the API.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
</section>
|
||
<section xml:id="_detect_image_labels_example">
|
||
<title>Detect Image Labels Example</title>
|
||
<simpara><link xl:href="https://cloud.google.com/vision/docs/detecting-labels">Image labeling</link> refers to producing labels that describe the contents of an image.
|
||
Below is a code sample of how this is done using the Cloud Vision Spring Template.</simpara>
|
||
<programlisting language="java" linenumbering="unnumbered">@Autowired
|
||
private ResourceLoader resourceLoader;
|
||
|
||
@Autowired
|
||
private CloudVisionTemplate cloudVisionTemplate;
|
||
|
||
public void processImage() {
|
||
Resource imageResource = this.resourceLoader.getResource("my_image.jpg");
|
||
AnnotateImageResponse response = this.cloudVisionTemplate.analyzeImage(
|
||
imageResource, Type.LABEL_DETECTION);
|
||
System.out.println("Image Classification results: " + response.getLabelAnnotationsList());
|
||
}</programlisting>
|
||
</section>
|
||
<section xml:id="_sample_12">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-vision-api-sample">Sample Spring Boot Application</link> is provided to show how to use the Cloud Vision starter and template.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_cloud_foundry">
|
||
<title>Cloud Foundry</title>
|
||
<simpara>Spring Cloud GCP provides support for Cloud Foundry’s <link xl:href="https://docs.pivotal.io/partners/gcp-sb/index.html">GCP Service Broker</link>.
|
||
Our Pub/Sub, Cloud Spanner, Storage, Stackdriver Trace and Cloud SQL MySQL and PostgreSQL starters are Cloud Foundry aware and retrieve properties like project ID, credentials, etc., that are used in auto configuration from the Cloud Foundry environment.</simpara>
|
||
<simpara>In cases like Pub/Sub’s topic and subscription, or Storage’s bucket name, where those parameters are not used in auto configuration, you can fetch them using the VCAP mapping provided by Spring Boot.
|
||
For example, to retrieve the provisioned Pub/Sub topic, you can use the <literal>vcap.services.mypubsub.credentials.topic_name</literal> property from the application environment.</simpara>
|
||
<note>
|
||
<simpara>If the same service is bound to the same application more than once, the auto configuration will not be able to choose among bindings and will not be activated for that service.
|
||
This includes both MySQL and PostgreSQL bindings to the same app.</simpara>
|
||
</note>
|
||
<warning>
|
||
<simpara>In order for the Cloud SQL integration to work in Cloud Foundry, auto-reconfiguration must be disabled.
|
||
You can do so using the <literal>cf set-env <APP> JBP_CONFIG_SPRING_AUTO_RECONFIGURATION '{enabled: false}'</literal> command.
|
||
Otherwise, Cloud Foundry will produce a <literal>DataSource</literal> with an invalid JDBC URL (i.e., <literal>jdbc:mysql://null/null</literal>).</simpara>
|
||
</warning>
|
||
</chapter>
|
||
<chapter xml:id="_kotlin_support">
|
||
<title>Kotlin Support</title>
|
||
<simpara>The latest version of the Spring Framework provides first-class support for Kotlin.
|
||
For Kotlin users of Spring, the Spring Cloud GCP libraries work out-of-the-box and are fully interoperable with Kotlin applications.</simpara>
|
||
<simpara>For more information on building a Spring application in Kotlin, please consult the <link xl:href="https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin">Spring Kotlin documentation</link>.</simpara>
|
||
<section xml:id="_prerequisites_2">
|
||
<title>Prerequisites</title>
|
||
<simpara>Ensure that your Kotlin application is properly set up.
|
||
Based on your build system, you will need to include the correct Kotlin build plugin in your project:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://kotlinlang.org/docs/reference/using-maven.html">Kotlin Maven Plugin</link></simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://kotlinlang.org/docs/reference/using-gradle.html">Kotlin Gradle Plugin</link></simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Depending on your application’s needs, you may need to augment your build configuration with compiler plugins:</simpara>
|
||
<itemizedlist>
|
||
<listitem>
|
||
<simpara><link xl:href="https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support">Kotlin Spring Plugin</link>: Makes your Spring configuration classes/members non-final for convenience.</simpara>
|
||
</listitem>
|
||
<listitem>
|
||
<simpara><link xl:href="https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support">Kotlin JPA Plugin</link>: Enables using JPA in Kotlin applications.</simpara>
|
||
</listitem>
|
||
</itemizedlist>
|
||
<simpara>Once your Kotlin project is properly configured, the Spring Cloud GCP libraries will work within your application without any additional setup.</simpara>
|
||
</section>
|
||
</chapter>
|
||
<chapter xml:id="_sample_13">
|
||
<title>Sample</title>
|
||
<simpara>A <link xl:href="https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample">Kotlin sample application</link> is provided to demonstrate a working Maven setup and various Spring Cloud GCP integrations from within Kotlin.</simpara>
|
||
</chapter>
|
||
</book> |