Skip to content

Commit

Permalink
Add support for Oracle's R2DBC driver.
Browse files Browse the repository at this point in the history
We support Oracle's experimental R2DBC driver by providing a dialect including bind markers. Since the driver is not yet available from Maven Central and it requires module-path support for ServiceLoader discovery, we need to apply a few workarounds including absence check for our integration tests.

See #230
  • Loading branch information
mp911de committed Mar 15, 2021
1 parent 174c5bd commit b7a7c2c
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 5 deletions.
29 changes: 29 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>21.1.0.0</version>
<scope>test</scope>
</dependency>

<!-- R2DBC Drivers -->

<dependency>
Expand Down Expand Up @@ -288,6 +295,12 @@
</exclusions>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-xe</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
Expand Down Expand Up @@ -448,6 +461,22 @@
</plugins>
</build>
</profile>

<profile>
<id>java11</id>

<!-- enable once oracle-r2db is available from Maven Central
<dependencies>
<dependency>
<groupId>com.oracle.database.r2dbc</groupId>
<artifactId>oracle-r2dbc</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
-->
</profile>
</profiles>

<repositories>
Expand Down
4 changes: 3 additions & 1 deletion src/main/asciidoc/new-features.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
[[new-features.1-2-0]]
== What's New in Spring Data R2DBC 1.2.0

* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC. Consult the <<upgrading.1.1-1.2,Migration Guide>> for further details.
* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC.
Consult the <<upgrading.1.1-1.2,Migration Guide>> for further details.
* Support for <<entity-callbacks>>.
* <<r2dbc.auditing,Auditing>> through `@EnableR2dbcAuditing`.
* Support for `@Value` in persistence constructors.
* Support for Oracle's R2DBC driver.

[[new-features.1-1-0]]
== What's New in Spring Data R2DBC 1.1.0
Expand Down
8 changes: 4 additions & 4 deletions src/main/asciidoc/reference/r2dbc-core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ There is a https://github.com/spring-projects/spring-data-examples[GitHub reposi
[[r2dbc.connecting]]
== Connecting to a Relational Database with Spring

One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object by using the IoC container. Make sure to use a <<r2dbc.drivers,supported database and driver>>.
One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object by using the IoC container.Make sure to use a <<r2dbc.drivers,supported database and driver>>.

[[r2dbc.connectionfactory]]
=== Registering a `ConnectionFactory` Instance using Java-based Metadata
Expand All @@ -173,7 +173,7 @@ public class ApplicationConfiguration extends AbstractR2dbcConfiguration {
----
====

This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in {spring-framework-ref}/data-access.html[Spring's DAO support features].
This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`.As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation.This hierarchy and the use of `@Repository` is described in {spring-framework-ref}/data-access.html[Spring's DAO support features].

`AbstractR2dbcConfiguration` also registers `DatabaseClient`, which is required for database interaction and for Repository implementation.

Expand All @@ -191,11 +191,11 @@ Spring Data R2DBC ships with dialect impelemtations for the following drivers:
* https://github.com/mirromutth/r2dbc-mysql[MySQL] (`dev.miku:r2dbc-mysql`)
* https://github.com/jasync-sql/jasync-sql[jasync-sql MySQL] (`com.github.jasync-sql:jasync-r2dbc-mysql`)
* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`)
* https://github.com/oracle/oracle-r2dbc[Oracle] (`com.oracle.database.r2dbc:oracle-r2dbc`)

Spring Data R2DBC reacts to database specifics by inspecting the `ConnectionFactory` and selects the appropriate database dialect accordingly.
You need to configure your own {spring-data-r2dbc-javadoc}/api/org/springframework/data/r2dbc/dialect/R2dbcDialect.html[`R2dbcDialect`] if the driver you use is not yet known to Spring Data R2DBC.

TIP: Dialects are resolved by {spring-data-r2dbc-javadoc}/org/springframework/data/r2dbc/dialect/DialectResolver.html[`DialectResolver`] from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`.
+
You can let Spring auto-discover your `R2dbcDialect` by registering a class that implements `org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider` through `META-INF/spring.factories`.
+ You can let Spring auto-discover your `R2dbcDialect` by registering a class that implements `org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider` through `META-INF/spring.factories`.
`DialectResolver` discovers dialect provider implementations from the class path using Spring's `SpringFactoriesLoader`.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ static class BuiltInDialectProvider implements R2dbcDialectProvider {
BUILTIN.put("Microsoft SQL Server", SqlServerDialect.INSTANCE);
BUILTIN.put("MySQL", MySqlDialect.INSTANCE);
BUILTIN.put("MariaDB", MySqlDialect.INSTANCE);
BUILTIN.put("Oracle", OracleDialect.INSTANCE);
BUILTIN.put("PostgreSQL", PostgresDialect.INSTANCE);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2021 the original author or authors.
*
* 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 org.springframework.data.r2dbc.dialect;

import org.springframework.r2dbc.core.binding.BindMarkersFactory;

/**
* An SQL dialect for Oracle.
*
* @author Mark Paluch
* @since 1.2.6
*/
public class OracleDialect extends org.springframework.data.relational.core.dialect.OracleDialect
implements R2dbcDialect {

/**
* Singleton instance.
*/
public static final OracleDialect INSTANCE = new OracleDialect();

private static final BindMarkersFactory NAMED = BindMarkersFactory.named(":", "P", 32,
OracleDialect::filterBindMarker);

/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory()
*/
@Override
public BindMarkersFactory getBindMarkersFactory() {
return NAMED;
}

private static String filterBindMarker(CharSequence input) {

StringBuilder builder = new StringBuilder();

for (int i = 0; i < input.length(); i++) {

char ch = input.charAt(i);

// ascii letter or digit
if (Character.isLetterOrDigit(ch) && ch < 127) {
builder.append(ch);
}
}

if (builder.length() == 0) {
return "";
}

return "_" + builder;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2018-2021 the original author or authors.
*
* 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 org.springframework.data.r2dbc.core;

import io.r2dbc.spi.ConnectionFactory;

import javax.sql.DataSource;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.RegisterExtension;

import org.springframework.data.r2dbc.testing.EnabledOnClass;
import org.springframework.data.r2dbc.testing.ExternalDatabase;
import org.springframework.data.r2dbc.testing.OracleTestSupport;

/**
* Integration tests for {@link DatabaseClient} against Oracle.
*
* @author Mark Paluch
*/
@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
public class OracleDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests {

@RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database();

@Override
protected DataSource createDataSource() {
return OracleTestSupport.createDataSource(database);
}

@Override
protected ConnectionFactory createConnectionFactory() {
return OracleTestSupport.createConnectionFactory(database);
}

@Override
protected String getCreateTableStatement() {
return OracleTestSupport.CREATE_TABLE_LEGOSET;
}

@Override
@Disabled("https://github.com/oracle/oracle-r2dbc/issues/9")
public void executeSelectNamedParameters() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2021 the original author or authors.
*
* 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 org.springframework.data.r2dbc.dialect;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;

import org.springframework.r2dbc.core.binding.BindMarker;
import org.springframework.r2dbc.core.binding.BindMarkers;

/**
* Unit tests for {@link OracleDialect}.
*
* @author Mark Paluch
*/
class OracleDialectUnitTests {

@Test // gh-230
void shouldUseNamedPlaceholders() {

BindMarkers bindMarkers = OracleDialect.INSTANCE.getBindMarkersFactory().create();

BindMarker first = bindMarkers.next();
BindMarker second = bindMarkers.next("'foo!bar");

assertThat(first.getPlaceholder()).isEqualTo(":P0");
assertThat(second.getPlaceholder()).isEqualTo(":P1_foobar");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2019-2021 the original author or authors.
*
* 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 org.springframework.data.r2dbc.repository;

import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.sql.DataSource;

import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
import org.springframework.data.r2dbc.testing.EnabledOnClass;
import org.springframework.data.r2dbc.testing.ExternalDatabase;
import org.springframework.data.r2dbc.testing.OracleTestSupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

/**
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Oracle.
*
* @author Mark Paluch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
public class OracleR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests {

@RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database();

@Configuration
@EnableR2dbcRepositories(considerNestedRepositories = true,
includeFilters = @Filter(classes = OracleLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE))
static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration {

@Bean
@Override
public ConnectionFactory connectionFactory() {
return OracleTestSupport.createConnectionFactory(database);
}
}

@Override
protected DataSource createDataSource() {
return OracleTestSupport.createDataSource(database);
}

@Override
protected ConnectionFactory createConnectionFactory() {
return OracleTestSupport.createConnectionFactory(database);
}

@Override
protected String getCreateTableStatement() {
return OracleTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION;
}

@Override
protected Class<? extends LegoSetRepository> getRepositoryInterfaceType() {
return OracleLegoSetRepository.class;
}

interface OracleLegoSetRepository extends LegoSetRepository {

@Override
@Query("SELECT name FROM legoset")
Flux<Named> findAsProjection();

@Override
@Query("SELECT * FROM legoset WHERE manual = :manual")
Mono<LegoSet> findByManual(int manual);

@Override
@Query("SELECT id FROM legoset")
Flux<Integer> findAllIds();
}
}
Loading

0 comments on commit b7a7c2c

Please sign in to comment.