diff --git a/pom.xml b/pom.xml index f0f6bc4..6e43082 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -31,8 +33,10 @@ 42.2.5 5.1.47 1.0.14 + 0.8.1-alpha1 0.8.0.RELEASE 7.1.2.jre8-preview + 2.5.4 Arabba-SR2 1.0.3 4.1.43.Final @@ -195,6 +199,13 @@ test + + org.mariadb.jdbc + mariadb-java-client + ${mariadb-jdbc.version} + test + + com.microsoft.sqlserver mssql-jdbc @@ -235,6 +246,13 @@ test + + org.mariadb + r2dbc-mariadb + ${r2dbc-mariadb.version} + test + + io.r2dbc r2dbc-spi-test @@ -256,6 +274,18 @@ + + org.testcontainers + mariadb + test + + + org.slf4j + jcl-over-slf4j + + + + org.testcontainers postgresql diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java b/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java index 9b11a37..667d58d 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/DialectResolver.java @@ -114,6 +114,7 @@ public class DialectResolver { BUILTIN.put("H2", H2Dialect.INSTANCE); BUILTIN.put("Microsoft SQL Server", SqlServerDialect.INSTANCE); BUILTIN.put("MySQL", MySqlDialect.INSTANCE); + BUILTIN.put("MariaDB", MySqlDialect.INSTANCE); BUILTIN.put("PostgreSQL", PostgresDialect.INSTANCE); } diff --git a/src/test/java/org/springframework/data/r2dbc/core/MariaDbDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/MariaDbDatabaseClientIntegrationTests.java new file mode 100644 index 0000000..436d460 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/core/MariaDbDatabaseClientIntegrationTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2019-2020 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 static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import lombok.Data; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.dialect.MySqlDialect; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MariaDbTestSupport; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for {@link DatabaseClient} against MariaDB. + * + * @author Mark Paluch + */ +public class MariaDbDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MariaDbTestSupport.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; + } + + @Test // gh-166 + public void considersBuiltInConverters() { + + ConnectionFactory connectionFactory = createConnectionFactory(); + JdbcTemplate jdbc = createJdbcTemplate(createDataSource()); + + try { + jdbc.execute("DROP TABLE boolean_mapping"); + } catch (DataAccessException e) {} + jdbc.execute("CREATE TABLE boolean_mapping (id int, flag1 TINYINT, flag2 TINYINT)"); + + BooleanMapping mapping = new BooleanMapping(); + mapping.setId(42); + mapping.setFlag1(true); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.insert().into(BooleanMapping.class).using(mapping).then() // + .as(StepVerifier::create) // + .verifyComplete(); + + databaseClient.select().from(BooleanMapping.class).fetch().first() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual.isFlag1()).isTrue()) // + .verifyComplete(); + } + + @Test // gh-305 + public void shouldApplyCustomConverters() { + + ConnectionFactory connectionFactory = createConnectionFactory(); + JdbcTemplate jdbc = createJdbcTemplate(createDataSource()); + ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(MySqlDialect.INSTANCE, + Arrays.asList(UuidToStringConverter.INSTANCE, StringToUuidConverter.INSTANCE)); + + try { + jdbc.execute("DROP TABLE uuid_type"); + } catch (DataAccessException e) {} + jdbc.execute("CREATE TABLE uuid_type (id varchar(255), uuid_value varchar(255))"); + + UuidType uuidType = new UuidType(); + uuidType.setId(UUID.randomUUID()); + uuidType.setUuidValue(UUID.randomUUID()); + + DatabaseClient databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) + .dataAccessStrategy(strategy).build(); + + databaseClient.insert().into(UuidType.class).using(uuidType).then() // + .as(StepVerifier::create) // + .verifyComplete(); + + databaseClient.select().from(UuidType.class).matching(Criteria.where("id").is(uuidType.getId())) // + .fetch().first() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual.getUuidValue()).isEqualTo(uuidType.getUuidValue())) // + .verifyComplete(); + + uuidType.setUuidValue(null); + databaseClient.update().table(UuidType.class).using(uuidType).then() // + .as(StepVerifier::create) // + .verifyComplete(); + + databaseClient.execute("SELECT * FROM uuid_type WHERE id = ?") // + .bind(0, uuidType.getId()) // + .as(UuidType.class) // + .fetch().first() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual.getUuidValue()).isNull()) // + .verifyComplete(); + + databaseClient.execute("SELECT * FROM uuid_type WHERE id in (:ids)") // + .bind("ids", Collections.singleton(uuidType.getId())) // + .as(UuidType.class) // + .fetch().first() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(actual.getUuidValue()).isNull()) // + .verifyComplete(); + } + + @Table("boolean_mapping") + @Data + static class BooleanMapping { + + int id; + boolean flag1; + boolean flag2; + } + + @Table("uuid_type") + @Data + static class UuidType { + + @Id UUID id; + UUID uuidValue; + } + + @WritingConverter + enum UuidToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(UUID uuid) { + return uuid.toString(); + } + } + + @ReadingConverter + enum StringToUuidConverter implements Converter { + INSTANCE; + + @Override + public UUID convert(String value) { + return UUID.fromString(value); + } + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/core/MariaDbTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/MariaDbTransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 0000000..28dcbed --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/core/MariaDbTransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2020 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 reactor.core.publisher.Mono; + +import java.time.Duration; + +import javax.sql.DataSource; + +import org.junit.ClassRule; + +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MariaDbTestSupport; + +/** + * Transactional integration tests for {@link DatabaseClient} against MariaDb. + * + * @author Mark Paluch + */ +public class MariaDbTransactionalDatabaseClientIntegrationTests + extends AbstractTransactionalDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MariaDbTestSupport.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; + } + + @Override + protected Mono prepareForTransaction(DatabaseClient client) { + + /* + * We have to execute a sql statement first. + * Otherwise Mariadb don't have a transaction id. + * And we need to delay emitting the result so that Mariadb has time to write the transaction id, which is done in + * batches every now and then. + */ + return client.execute(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated() // + .delayElement(Duration.ofMillis(50)) // + .then(); + } + + @Override + protected String getCurrentTransactionIdStatement() { + return "SELECT tx.trx_id FROM information_schema.innodb_trx tx WHERE tx.trx_mysql_thread_id = connection_id()"; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java new file mode 100644 index 0000000..0bff5c5 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 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.ClassRule; +import org.junit.runner.RunWith; + +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.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against MariaDB. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class MariaDbR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @ClassRule 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 * FROM legoset WHERE name like ?") + Flux findByNameContains(String name); + + @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/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java new file mode 100644 index 0000000..0020a37 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/MariaDbTestSupport.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019-2020 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; + +/** + * Utility class for testing against MariaDB. + * + * @author Mark Paluch + */ +public class MariaDbTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static 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 String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + /** + * 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 at {@code postgres:@localhost:5432/postgres}. + */ + 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 { + MariaDBContainer container = new MariaDBContainer(); + container.start(); + + testContainerDatabase = ProvidedDatabase.builder(container) // + .username("root") // + .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.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPort(database.getPort()); + + return dataSource; + } +}