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
This commit is contained in:
Mark Paluch
2021-03-15 10:14:43 +01:00
parent 23c14af294
commit 8f97d37558
13 changed files with 646 additions and 5 deletions

View File

@@ -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() {}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.testing;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* {@code @EnabledOnClass} is used to signal that the annotated test class or test method is only <em>enabled</em> if
* the specified {@link #value() class} is present.
* <p>
* When applied at the class level, all test methods within that class will be enabled on presence of the specified
* class.
* <p>
* If a test method is disabled via this annotation, that does not prevent the test class from being instantiated.
* Rather, it prevents the execution of the test method and method-level lifecycle callbacks such as {@code @BeforeEach}
* methods, {@code @AfterEach} methods, and corresponding extension APIs.
* <p>
* This annotation may be used as a meta-annotation in order to create a custom <em>composed annotation</em> that
* inherits the semantics of this annotation.
*
* @see JRE
* @see org.junit.jupiter.api.Disabled
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(EnabledOnClassCondition.class)
public @interface EnabledOnClass {
String value();
}

View File

@@ -0,0 +1,52 @@
/*
* 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.testing;
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.*;
import static org.junit.platform.commons.util.AnnotationUtils.*;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.util.ClassUtils;
/**
* {@link ExecutionCondition} for {@link EnabledOnClass @EnabledOnClass}.
*
* @author Mark Paluch
* @see EnabledOnClass
*/
class EnabledOnClassCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
return findAnnotation(context.getElement(), EnabledOnClass.class) //
.map(annotation -> isEnabled(annotation)
? enabled(String.format("Class '%s' found on the class path.", annotation.value()))
: disabled(String.format("Class '%s' not found on the class path.", annotation.value()))) //
.orElseGet(this::enabledByDefault);
}
private boolean isEnabled(EnabledOnClass annotation) {
return ClassUtils.isPresent(annotation.value(), EnabledOnClassCondition.class.getClassLoader());
}
private ConditionEvaluationResult enabledByDefault() {
return enabled("@EnabledOnClass is not present");
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.testing;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryProvider;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link ConnectionFactoryProvider} for Oracle's R2DBC driver. Allows for absence of the driver which is required when
* using Java 8 as the ServiceLoader requires presence of classes listed in the service loader manifest.
*
* @author Mark Paluch
*/
public class OracleConnectionFactoryProviderWrapper implements ConnectionFactoryProvider {
private final ConnectionFactoryProvider delegate;
public OracleConnectionFactoryProviderWrapper() {
if (ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl", getClass().getClassLoader())) {
delegate = createProvider();
} else {
delegate = null;
}
}
private static ConnectionFactoryProvider createProvider() {
try {
return (ConnectionFactoryProvider) Class.forName("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl")
.newInstance();
} catch (ReflectiveOperationException e) {
ReflectionUtils.handleReflectionException(e);
}
return null;
}
@Override
public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) {
if (delegate != null) {
return delegate.create(connectionFactoryOptions);
}
throw new IllegalStateException(
"Oracle R2DBC (oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl) is not on the class path");
}
@Override
public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) {
if (delegate != null) {
return delegate.supports(connectionFactoryOptions);
}
return false;
}
@Override
public String getDriver() {
if (delegate != null) {
return delegate.getDriver();
}
return "oracle-r2dbc (proxy)";
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.testing;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.util.ClassUtils;
import org.testcontainers.containers.OracleContainer;
/**
* Utility class for testing against Oracle.
*
* @author Mark Paluch
*/
public class OracleTestSupport {
private static ExternalDatabase testContainerDatabase;
public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" //
+ " id INTEGER PRIMARY KEY,\n" //
+ " version INTEGER NULL,\n" //
+ " name VARCHAR2(255) NOT NULL,\n" //
+ " manual INTEGER NULL,\n" //
+ " cert RAW(255) NULL\n" //
+ ")";
public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" //
+ " id INTEGER GENERATED by default on null as IDENTITY PRIMARY KEY,\n" //
+ " version INTEGER NULL,\n" //
+ " name VARCHAR2(255) NOT NULL,\n" //
+ " manual INTEGER NULL\n" //
+ ")";
/**
* Returns a database either hosted locally or running inside Docker.
*
* @return information about the database. Guaranteed to be not {@literal null}.
*/
public static ExternalDatabase database() {
if (!ClassUtils.isPresent("oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl",
OracleTestSupport.class.getClassLoader())) {
return ExternalDatabase.unavailable();
}
if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) {
return getFirstWorkingDatabase( //
OracleTestSupport::local, //
OracleTestSupport::testContainer //
);
} else {
return getFirstWorkingDatabase( //
OracleTestSupport::testContainer, //
OracleTestSupport::local //
);
}
}
@SafeVarargs
private static ExternalDatabase getFirstWorkingDatabase(Supplier<ExternalDatabase>... suppliers) {
return Stream.of(suppliers).map(Supplier::get) //
.filter(ExternalDatabase::checkValidity) //
.findFirst() //
.orElse(ExternalDatabase.unavailable());
}
/**
* Returns a locally provided database.
*/
private static ExternalDatabase local() {
return ProvidedDatabase.builder() //
.hostname("localhost") //
.port(1521) //
.database("XEPDB1") //
.username("system") //
.password("oracle") //
.jdbcUrl("jdbc:oracle:thin:system/oracle@localhost:1521:XEPDB1") //
.build();
}
/**
* Returns a database provided via Testcontainers.
*/
private static ExternalDatabase testContainer() {
if (testContainerDatabase == null) {
try {
OracleContainer container = new OracleContainer("springci/spring-data-oracle-xe-prebuild:18.4.0")
.withReuse(true);
container.start();
testContainerDatabase = ProvidedDatabase.builder(container) //
.database("XEPDB1").build();
} catch (IllegalStateException ise) {
// docker not available.
testContainerDatabase = ExternalDatabase.unavailable();
}
}
return testContainerDatabase;
}
/**
* Creates a new Oracle {@link ConnectionFactory} configured from the {@link ExternalDatabase}.
*/
public static ConnectionFactory createConnectionFactory(ExternalDatabase database) {
ConnectionFactoryOptions options = ConnectionUtils.createOptions("oracle", database);
return ConnectionFactories.get(options);
}
/**
* Creates a new {@link DataSource} configured from the {@link ExternalDatabase}.
*/
public static DataSource createDataSource(ExternalDatabase database) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUsername(database.getUsername());
dataSource.setPassword(database.getPassword());
dataSource.setUrl(database.getJdbcUrl().replace(":xe", "/XEPDB1"));
return dataSource;
}
}

View File

@@ -0,0 +1,2 @@
# https://github.com/oracle/oracle-r2dbc/issues/10, otherwise the driver needs to be operated in module-path mode.
org.springframework.data.r2dbc.testing.OracleConnectionFactoryProviderWrapper