Skip to content

Commit

Permalink
Upgrade to modern Spring Data R2DBC (#347)
Browse files Browse the repository at this point in the history
Spring Data dialect updates:
* Many supporting classes moved from Spring Data into Spring Framework.
* Spring Boot integration is no longer in experimental; it's part of mainstream Boot autoconfiguration.
* The need for `SpannerBindMarkerFactoryProvider` is a bit redundant since the same information can be derived from dialect, which is already getting autodiscovered. But it's documented [here](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#r2dbc-DatabaseClient).

Updated sample:
* Explicitly specifying `@Column` is needed because v1 is case-sensitive. We have fixed it to comply with case-insensitive spec for v2 in #271 , so this should become unnecessary when we migrate to v2.

Necessary but not sufficient step towards #314.
  • Loading branch information
elefeint authored Mar 29, 2021
1 parent c27f679 commit 6efbf51
Show file tree
Hide file tree
Showing 16 changed files with 229 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@
<artifactId>cloud-spanner-r2dbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-bom-r2dbc</artifactId>
<version>0.1.0.M3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand All @@ -44,26 +37,13 @@
<version>${project.version}</version>
</dependency>

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

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

</dependencies>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>

<build>
<plugins>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.example;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

/**
Expand All @@ -26,8 +27,10 @@
public class Book {

@Id
@Column("ID")
private String id;

@Column("TITLE")
private String title;

public Book(String id, String title) {
Expand All @@ -42,4 +45,5 @@ public String getId() {
public String getTitle() {
return this.title;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Hooks;

/**
* Driver application showing Cloud Spanner R2DBC use with Spring Data.
Expand All @@ -62,6 +63,7 @@ public class SpringDataR2dbcApp {
private DatabaseClient r2dbcClient;

public static void main(String[] args) {
Hooks.onOperatorDebug();
Assert.notNull(INSTANCE, "Please provide spanner.instance property");
Assert.notNull(DATABASE, "Please provide spanner.database property");
Assert.notNull(GCP_PROJECT, "Please provide gcp.project property");
Expand All @@ -86,11 +88,12 @@ public static ConnectionFactory spannerConnectionFactory() {
public void setUpData() {
LOGGER.info("Setting up test table BOOK...");
try {
r2dbcClient.execute("CREATE TABLE BOOK ("
r2dbcClient.sql("CREATE TABLE BOOK ("
+ " ID STRING(36) NOT NULL,"
+ " TITLE STRING(MAX) NOT NULL"
+ ") PRIMARY KEY (ID)")
.fetch().rowsUpdated().block();
+ ") PRIMARY KEY (ID)"
).fetch().rowsUpdated().block();

} catch (Exception e) {
LOGGER.info("Failed to set up test table BOOK", e);
return;
Expand All @@ -102,7 +105,7 @@ public void setUpData() {
public void tearDownData() {
LOGGER.info("Deleting test table BOOK...");
try {
r2dbcClient.execute("DROP TABLE BOOK")
r2dbcClient.sql("DROP TABLE BOOK")
.fetch().rowsUpdated().block();
} catch (Exception e) {
LOGGER.info("Failed to delete test table BOOK", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@

import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
Expand All @@ -40,24 +39,23 @@
public class WebController {

@Autowired
private DatabaseClient r2dbcClient;
private R2dbcEntityTemplate r2dbcEntityTemplate;

@Autowired
private BookRepository r2dbcRepository;

@GetMapping("/list")
public Flux<Book> listBooks() {
return r2dbcClient.execute("SELECT id, title FROM BOOK")
.as(Book.class)
.fetch().all();
return r2dbcEntityTemplate
.select(Book.class)
.all();
}

@PostMapping("/add")
public Mono<Void> addBook(@RequestBody String bookTitle) {
return r2dbcClient.insert()
.into("book")
.value("id", UUID.randomUUID().toString())
.value("title", bookTitle)
return r2dbcEntityTemplate.insert(Book.class)
.using(new Book(UUID.randomUUID().toString(), bookTitle))
.log()
.then();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
logging.level.org.springframework.r2dbc=DEBUG
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration;
import com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryMetadata;
import com.google.cloud.spanner.r2dbc.util.Assert;
import io.r2dbc.spi.Closeable;
import io.r2dbc.spi.Connection;
Expand Down Expand Up @@ -54,7 +55,7 @@ public Publisher<? extends Connection> create() {

@Override
public ConnectionFactoryMetadata getMetadata() {
return null;
return SpannerConnectionFactoryMetadata.INSTANCE;
}

/**
Expand All @@ -66,7 +67,7 @@ public ConnectionFactoryMetadata getMetadata() {
* @return A Mono indicating that the blocking call completed
*/
@Override
public Publisher<Void> close() {
public Mono<Void> close() {
return Mono.fromRunnable(() -> this.spannerClient.close());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration;
import com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryMetadata;
import io.r2dbc.spi.Connection;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -87,4 +88,14 @@ void connectionFactoryClosingResultsInSpannerClientClosure() {
verifyNoMoreInteractions(mockSpanner);

}

@Test
void testGetMetadata() {
SpannerClientLibraryConnectionFactory cf =
new SpannerClientLibraryConnectionFactory(this.configBuilder.build());

assertThat(cf.getMetadata()).isSameAs(SpannerConnectionFactoryMetadata.INSTANCE);

cf.close().block();
}
}
2 changes: 1 addition & 1 deletion cloud-spanner-spring-data-r2dbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<artifactId>cloud-spanner-spring-data-r2dbc</artifactId>

<properties>
<spring-data-r2dbc.version>1.0.0.RELEASE</spring-data-r2dbc.version>
<spring-data-r2dbc.version>1.2.6</spring-data-r2dbc.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.r2dbc.springdata;

import com.google.cloud.spanner.r2dbc.SpannerConnectionFactoryMetadata;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
import org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver.BindMarkerFactoryProvider;

/**
* Provides the named bind marker strategy for Cloud Spanner.
*/
public class SpannerBindMarkerFactoryProvider implements BindMarkerFactoryProvider {

@Override
public BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory) {
if (SpannerConnectionFactoryMetadata.INSTANCE.equals(connectionFactory.getMetadata())) {
return SpannerR2dbcDialect.NAMED;
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@

package com.google.cloud.spanner.r2dbc.springdata;

import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.relational.core.dialect.AbstractDialect;
import org.springframework.data.relational.core.dialect.LimitClause;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;

/**
* The {@link R2dbcDialect} implementation which enables usage of Spring Data R2DBC with Cloud
* Spanner.
*/
public class SpannerR2dbcDialect extends AbstractDialect implements R2dbcDialect {
private static final BindMarkersFactory NAMED =
static final BindMarkersFactory NAMED =
BindMarkersFactory.named("@", "val", 32);

public static final String SQL_LIMIT = "LIMIT ";
Expand All @@ -53,6 +55,24 @@ public Position getClausePosition() {
}
};

/**
* Pessimistic locking is not supported.
* Spanner has a LOCK_SCANNED_RANGES hint, but it appears before SELECT, a position not currently
* supported in LockClause.Position
*/
private static final LockClause LOCK_CLAUSE = new LockClause() {
@Override
public String getLock(LockOptions lockOptions) {
return "";
}

@Override
public Position getClausePosition() {
// It does not matter where to append an empty string.
return Position.AFTER_FROM_TABLE;
}
};

@Override
public BindMarkersFactory getBindMarkersFactory() {
return NAMED;
Expand All @@ -62,4 +82,9 @@ public BindMarkersFactory getBindMarkersFactory() {
public LimitClause limit() {
return LIMIT_CLAUSE;
}

@Override
public LockClause lock() {
return LOCK_CLAUSE;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider=\
com.google.cloud.spanner.r2dbc.springdata.SpannerR2dbcDialectProvider
org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider=com.google.cloud.spanner.r2dbc.springdata.SpannerBindMarkerFactoryProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2021-2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.r2dbc.springdata;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.r2dbc.SpannerConnectionConfiguration;
import com.google.cloud.spanner.r2dbc.SpannerConnectionFactory;
import com.google.cloud.spanner.r2dbc.client.Client;
import com.google.cloud.spanner.r2dbc.v2.SpannerClientLibraryConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import org.junit.jupiter.api.Test;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;

class SpannerBindMarkerFactoryProviderTest {

@Test
void spannerBindMarkersFoundForV1ConnectionFactory() {
SpannerBindMarkerFactoryProvider provider = new SpannerBindMarkerFactoryProvider();
SpannerConnectionFactory cf = new SpannerConnectionFactory(
mock(Client.class), mock(SpannerConnectionConfiguration.class));

BindMarkersFactory factory = provider.getBindMarkers(cf);
assertThat(factory).isSameAs(SpannerR2dbcDialect.NAMED);
}

@Test
void spannerBindMarkersFoundForV2ConnectionFactory() {
SpannerBindMarkerFactoryProvider provider = new SpannerBindMarkerFactoryProvider();
SpannerConnectionConfiguration mockConfig = mock(SpannerConnectionConfiguration.class);
SpannerOptions mockSpannerOptions = mock(SpannerOptions.class);
Spanner mockService = mock(Spanner.class);

when(mockConfig.buildSpannerOptions()).thenReturn(mockSpannerOptions);
when(mockSpannerOptions.getService()).thenReturn(mockService);

SpannerClientLibraryConnectionFactory cf =
new SpannerClientLibraryConnectionFactory(mockConfig);

BindMarkersFactory factory = provider.getBindMarkers(cf);
assertThat(factory).isSameAs(SpannerR2dbcDialect.NAMED);
}

@Test
void spannerBindMarkersNotFoundForUnknownFactory() {
SpannerBindMarkerFactoryProvider provider = new SpannerBindMarkerFactoryProvider();
ConnectionFactory cf = mock(ConnectionFactory.class);
ConnectionFactoryMetadata mockMetadata = mock(ConnectionFactoryMetadata.class);
when(cf.getMetadata()).thenReturn(mockMetadata);
when(mockMetadata.getName()).thenReturn("SOME_DATABASE");

BindMarkersFactory factory = provider.getBindMarkers(cf);
assertThat(factory).isNull();
}

}
Loading

0 comments on commit 6efbf51

Please sign in to comment.