diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index e0162559c..1b6e10540 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -28,6 +28,7 @@ 0.1.4 1.0.1.RELEASE 1.0.0.RELEASE + 1.1.3 1.0.0.RELEASE 1.0.0 1.0.0.RELEASE @@ -190,6 +191,13 @@ test + + org.mariadb.jdbc + mariadb-java-client + ${mariadb-java-client.version} + test + + com.oracle.database.jdbc ojdbc11 @@ -213,6 +221,13 @@ test + + org.mariadb + r2dbc-mariadb + ${r2dbc-mariadb.version} + test + + io.r2dbc r2dbc-mssql diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index cb092b037..afe50d096 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -598,6 +598,10 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R return (row, metadata) -> { + if (metadata == null) { + metadata = row.getMetadata(); + } + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java index b632670e5..dfbb67b75 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java @@ -18,11 +18,6 @@ package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.RowMetadata; -import java.lang.reflect.Method; - -import org.springframework.lang.Nullable; -import org.springframework.util.ReflectionUtils; - /** * Utility methods for {@link io.r2dbc.spi.RowMetadata} * @@ -31,9 +26,6 @@ import org.springframework.util.ReflectionUtils; */ class RowMetadataUtils { - private static final @Nullable Method getColumnMetadatas = ReflectionUtils.findMethod(RowMetadata.class, - "getColumnMetadatas"); - /** * Check whether the column {@code name} is contained in {@link RowMetadata}. The check happens case-insensitive. * @@ -63,12 +55,6 @@ class RowMetadataUtils { */ @SuppressWarnings("unchecked") public static Iterable getColumnMetadata(RowMetadata metadata) { - - if (getColumnMetadatas != null) { - // Return type of RowMetadata.getColumnMetadatas was updated with R2DBC 0.9. - return (Iterable) ReflectionUtils.invokeMethod(getColumnMetadatas, metadata); - } - return metadata.getColumnMetadatas(); } } diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java index a1e82b705..6efb1372d 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java @@ -14,8 +14,9 @@ import lombok.RequiredArgsConstructor; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.mariadb.r2dbc.MariadbConnectionConfiguration; +import org.mariadb.r2dbc.MariadbConnectionFactory; import org.reactivestreams.Publisher; - import org.springframework.data.relational.core.dialect.LimitClause; import org.springframework.data.relational.core.dialect.LockClause; import org.springframework.data.relational.core.sql.LockOptions; @@ -35,9 +36,12 @@ public class DialectResolverUnitTests { PostgresqlConnectionFactory postgres = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() .host("localhost").database("foo").username("bar").password("password").build()); H2ConnectionFactory h2 = new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("mem").build()); + MariadbConnectionFactory mariadb = new MariadbConnectionFactory( + MariadbConnectionConfiguration.builder().socket("/foo").username("bar").build()); assertThat(DialectResolver.getDialect(postgres)).isEqualTo(PostgresDialect.INSTANCE); assertThat(DialectResolver.getDialect(h2)).isEqualTo(H2Dialect.INSTANCE); + assertThat(DialectResolver.getDialect(mariadb)).isEqualTo(MySqlDialect.INSTANCE); } @Test // gh-20, gh-104 diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java new file mode 100644 index 000000000..0cbccf6e1 --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019-2023 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.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MariaDbTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against MariaDB. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class MariaDbR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @RegisterExtension public static final ExternalDatabase database = MariaDbTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = MySqlLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Bean + @Override + public ConnectionFactory connectionFactory() { + return MariaDbTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return MariaDbTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MariaDbTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MariaDbTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return MySqlLegoSetRepository.class; + } + + interface MySqlLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT name FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java new file mode 100644 index 000000000..c2d1b8c19 --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java @@ -0,0 +1,167 @@ +/* + * Copyright 2019-2023 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 lombok.SneakyThrows; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.mariadb.jdbc.MariaDbDataSource; +import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Utility class for testing against MariaDB. + * + * @author Mark Paluch + * @author Jens Schauder + */ +public class MariaDbTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static final String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n," // + + " cert varbinary(255) NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " flag boolean NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String CREATE_TABLE_LEGOSET_WITH_MIXED_CASE_NAMES = "CREATE TABLE `LegoSet` (\n" // + + " `Id` integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " `Name` varchar(255) NOT NULL,\n" // + + " `Manual` integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String DROP_TABLE_LEGOSET_WITH_MIXED_CASE_NAMES = "DROP TABLE `LegoSet`"; + + /** + * Returns a database either hosted locally at {@code localhost:3306/mysql} or running inside Docker. + * + * @return information about the database. Guaranteed to be not {@literal null}. + */ + public static ExternalDatabase database() { + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + MariaDbTestSupport::local, // + MariaDbTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + MariaDbTestSupport::testContainer, // + MariaDbTestSupport::local // + ); + } + } + + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... 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(3306) // + .database("mysql") // + .username("root") // + .password("my-secret-pw") // + .jdbcUrl("jdbc:mariadb://localhost:3306/mysql") // + .build(); + } + + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + + String osArch = System.getProperty("os.arch"); + + DockerImageName armImageName = DockerImageName.parse("arm64v8/mariadb:10.3") + .asCompatibleSubstituteFor("mariadb"); + + DockerImageName mariadb = DockerImageName.parse("mariadb").withTag("10.3.6"); + var container = new MariaDBContainer<>("aarch64".equals(osArch) ? armImageName : mariadb); + + container.start(); + + testContainerDatabase = ProvidedDatabase.builder(container) // + .username("root") // + .database(container.getDatabaseName()) // + .build(); + } catch (IllegalStateException ise) { + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); + } + } + + return testContainerDatabase; + } + + /** + * Creates a new R2DBC MariaDB {@link ConnectionFactory} configured from the {@link ExternalDatabase}. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + + ConnectionFactoryOptions options = ConnectionUtils.createOptions("mariadb", database); + return new MariadbConnectionFactoryProvider().create(options); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + @SneakyThrows + public static DataSource createDataSource(ExternalDatabase database) { + + MariaDbDataSource dataSource = new MariaDbDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setUrl( + String.format("jdbc:mariadb://%s:%d/%s?", database.getHostname(), database.getPort(), database.getDatabase())); + + return dataSource; + } +}