Skip to content

Commit b7a7c2c

Browse files
committed
Add support for Oracle's R2DBC driver.
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
1 parent 174c5bd commit b7a7c2c

13 files changed

+646
-5
lines changed

pom.xml

+29
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@
216216
<scope>test</scope>
217217
</dependency>
218218

219+
<dependency>
220+
<groupId>com.oracle.database.jdbc</groupId>
221+
<artifactId>ojdbc11</artifactId>
222+
<version>21.1.0.0</version>
223+
<scope>test</scope>
224+
</dependency>
225+
219226
<!-- R2DBC Drivers -->
220227

221228
<dependency>
@@ -288,6 +295,12 @@
288295
</exclusions>
289296
</dependency>
290297

298+
<dependency>
299+
<groupId>org.testcontainers</groupId>
300+
<artifactId>oracle-xe</artifactId>
301+
<scope>test</scope>
302+
</dependency>
303+
291304
<dependency>
292305
<groupId>org.testcontainers</groupId>
293306
<artifactId>postgresql</artifactId>
@@ -448,6 +461,22 @@
448461
</plugins>
449462
</build>
450463
</profile>
464+
465+
<profile>
466+
<id>java11</id>
467+
468+
<!-- enable once oracle-r2db is available from Maven Central
469+
<dependencies>
470+
<dependency>
471+
<groupId>com.oracle.database.r2dbc</groupId>
472+
<artifactId>oracle-r2dbc</artifactId>
473+
<version>0.1.0</version>
474+
<scope>test</scope>
475+
</dependency>
476+
</dependencies>
477+
478+
-->
479+
</profile>
451480
</profiles>
452481

453482
<repositories>

src/main/asciidoc/new-features.adoc

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
[[new-features.1-2-0]]
1010
== What's New in Spring Data R2DBC 1.2.0
1111

12-
* 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.
12+
* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC.
13+
Consult the <<upgrading.1.1-1.2,Migration Guide>> for further details.
1314
* Support for <<entity-callbacks>>.
1415
* <<r2dbc.auditing,Auditing>> through `@EnableR2dbcAuditing`.
1516
* Support for `@Value` in persistence constructors.
17+
* Support for Oracle's R2DBC driver.
1618

1719
[[new-features.1-1-0]]
1820
== What's New in Spring Data R2DBC 1.1.0

src/main/asciidoc/reference/r2dbc-core.adoc

+4-4
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ There is a https://github.com/spring-projects/spring-data-examples[GitHub reposi
150150
[[r2dbc.connecting]]
151151
== Connecting to a Relational Database with Spring
152152

153-
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>>.
153+
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>>.
154154

155155
[[r2dbc.connectionfactory]]
156156
=== Registering a `ConnectionFactory` Instance using Java-based Metadata
@@ -173,7 +173,7 @@ public class ApplicationConfiguration extends AbstractR2dbcConfiguration {
173173
----
174174
====
175175

176-
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].
176+
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].
177177

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

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

195196
Spring Data R2DBC reacts to database specifics by inspecting the `ConnectionFactory` and selects the appropriate database dialect accordingly.
196197
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.
197198

198199
TIP: Dialects are resolved by {spring-data-r2dbc-javadoc}/org/springframework/data/r2dbc/dialect/DialectResolver.html[`DialectResolver`] from a `ConnectionFactory`, typically by inspecting `ConnectionFactoryMetadata`.
199-
+
200-
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`.
200+
+ 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`.
201201
`DialectResolver` discovers dialect provider implementations from the class path using Spring's `SpringFactoriesLoader`.

src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ static class BuiltInDialectProvider implements R2dbcDialectProvider {
115115
BUILTIN.put("Microsoft SQL Server", SqlServerDialect.INSTANCE);
116116
BUILTIN.put("MySQL", MySqlDialect.INSTANCE);
117117
BUILTIN.put("MariaDB", MySqlDialect.INSTANCE);
118+
BUILTIN.put("Oracle", OracleDialect.INSTANCE);
118119
BUILTIN.put("PostgreSQL", PostgresDialect.INSTANCE);
119120
}
120121

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.dialect;
17+
18+
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
19+
20+
/**
21+
* An SQL dialect for Oracle.
22+
*
23+
* @author Mark Paluch
24+
* @since 1.2.6
25+
*/
26+
public class OracleDialect extends org.springframework.data.relational.core.dialect.OracleDialect
27+
implements R2dbcDialect {
28+
29+
/**
30+
* Singleton instance.
31+
*/
32+
public static final OracleDialect INSTANCE = new OracleDialect();
33+
34+
private static final BindMarkersFactory NAMED = BindMarkersFactory.named(":", "P", 32,
35+
OracleDialect::filterBindMarker);
36+
37+
/*
38+
* (non-Javadoc)
39+
* @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory()
40+
*/
41+
@Override
42+
public BindMarkersFactory getBindMarkersFactory() {
43+
return NAMED;
44+
}
45+
46+
private static String filterBindMarker(CharSequence input) {
47+
48+
StringBuilder builder = new StringBuilder();
49+
50+
for (int i = 0; i < input.length(); i++) {
51+
52+
char ch = input.charAt(i);
53+
54+
// ascii letter or digit
55+
if (Character.isLetterOrDigit(ch) && ch < 127) {
56+
builder.append(ch);
57+
}
58+
}
59+
60+
if (builder.length() == 0) {
61+
return "";
62+
}
63+
64+
return "_" + builder;
65+
}
66+
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2018-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.core;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
20+
import javax.sql.DataSource;
21+
22+
import org.junit.jupiter.api.Disabled;
23+
import org.junit.jupiter.api.extension.RegisterExtension;
24+
25+
import org.springframework.data.r2dbc.testing.EnabledOnClass;
26+
import org.springframework.data.r2dbc.testing.ExternalDatabase;
27+
import org.springframework.data.r2dbc.testing.OracleTestSupport;
28+
29+
/**
30+
* Integration tests for {@link DatabaseClient} against Oracle.
31+
*
32+
* @author Mark Paluch
33+
*/
34+
@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
35+
public class OracleDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests {
36+
37+
@RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database();
38+
39+
@Override
40+
protected DataSource createDataSource() {
41+
return OracleTestSupport.createDataSource(database);
42+
}
43+
44+
@Override
45+
protected ConnectionFactory createConnectionFactory() {
46+
return OracleTestSupport.createConnectionFactory(database);
47+
}
48+
49+
@Override
50+
protected String getCreateTableStatement() {
51+
return OracleTestSupport.CREATE_TABLE_LEGOSET;
52+
}
53+
54+
@Override
55+
@Disabled("https://github.com/oracle/oracle-r2dbc/issues/9")
56+
public void executeSelectNamedParameters() {}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.dialect;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.r2dbc.core.binding.BindMarker;
23+
import org.springframework.r2dbc.core.binding.BindMarkers;
24+
25+
/**
26+
* Unit tests for {@link OracleDialect}.
27+
*
28+
* @author Mark Paluch
29+
*/
30+
class OracleDialectUnitTests {
31+
32+
@Test // gh-230
33+
void shouldUseNamedPlaceholders() {
34+
35+
BindMarkers bindMarkers = OracleDialect.INSTANCE.getBindMarkersFactory().create();
36+
37+
BindMarker first = bindMarkers.next();
38+
BindMarker second = bindMarkers.next("'foo!bar");
39+
40+
assertThat(first.getPlaceholder()).isEqualTo(":P0");
41+
assertThat(second.getPlaceholder()).isEqualTo(":P1_foobar");
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2019-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.repository;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
import reactor.core.publisher.Flux;
20+
import reactor.core.publisher.Mono;
21+
22+
import javax.sql.DataSource;
23+
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.junit.jupiter.api.extension.RegisterExtension;
26+
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.ComponentScan.Filter;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.context.annotation.FilterType;
31+
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
32+
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
33+
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
34+
import org.springframework.data.r2dbc.testing.EnabledOnClass;
35+
import org.springframework.data.r2dbc.testing.ExternalDatabase;
36+
import org.springframework.data.r2dbc.testing.OracleTestSupport;
37+
import org.springframework.test.context.ContextConfiguration;
38+
import org.springframework.test.context.junit.jupiter.SpringExtension;
39+
40+
/**
41+
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Oracle.
42+
*
43+
* @author Mark Paluch
44+
*/
45+
@ExtendWith(SpringExtension.class)
46+
@ContextConfiguration
47+
@EnabledOnClass("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
48+
public class OracleR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests {
49+
50+
@RegisterExtension public static final ExternalDatabase database = OracleTestSupport.database();
51+
52+
@Configuration
53+
@EnableR2dbcRepositories(considerNestedRepositories = true,
54+
includeFilters = @Filter(classes = OracleLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE))
55+
static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration {
56+
57+
@Bean
58+
@Override
59+
public ConnectionFactory connectionFactory() {
60+
return OracleTestSupport.createConnectionFactory(database);
61+
}
62+
}
63+
64+
@Override
65+
protected DataSource createDataSource() {
66+
return OracleTestSupport.createDataSource(database);
67+
}
68+
69+
@Override
70+
protected ConnectionFactory createConnectionFactory() {
71+
return OracleTestSupport.createConnectionFactory(database);
72+
}
73+
74+
@Override
75+
protected String getCreateTableStatement() {
76+
return OracleTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION;
77+
}
78+
79+
@Override
80+
protected Class<? extends LegoSetRepository> getRepositoryInterfaceType() {
81+
return OracleLegoSetRepository.class;
82+
}
83+
84+
interface OracleLegoSetRepository extends LegoSetRepository {
85+
86+
@Override
87+
@Query("SELECT name FROM legoset")
88+
Flux<Named> findAsProjection();
89+
90+
@Override
91+
@Query("SELECT * FROM legoset WHERE manual = :manual")
92+
Mono<LegoSet> findByManual(int manual);
93+
94+
@Override
95+
@Query("SELECT id FROM legoset")
96+
Flux<Integer> findAllIds();
97+
}
98+
}

0 commit comments

Comments
 (0)