#42 - Add dialect resolution for r2dbc-mariadb driver.

We now use the MySQL dialect when using the R2DBC MariaDB driver.
This commit is contained in:
Mark Paluch
2020-03-05 13:21:51 +01:00
parent 7ed68ee532
commit 341c796816
6 changed files with 544 additions and 1 deletions

32
pom.xml
View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@@ -31,8 +33,10 @@
<postgresql.version>42.2.5</postgresql.version>
<mysql.version>5.1.47</mysql.version>
<jasync.version>1.0.14</jasync.version>
<r2dbc-mariadb.version>0.8.1-alpha1</r2dbc-mariadb.version>
<r2dbc-spi-test.version>0.8.0.RELEASE</r2dbc-spi-test.version>
<mssql-jdbc.version>7.1.2.jre8-preview</mssql-jdbc.version>
<mariadb-jdbc.version>2.5.4</mariadb-jdbc.version>
<r2dbc-releasetrain.version>Arabba-SR2</r2dbc-releasetrain.version>
<reactive-streams.version>1.0.3</reactive-streams.version>
<netty>4.1.43.Final</netty>
@@ -195,6 +199,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>${mariadb-jdbc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
@@ -235,6 +246,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mariadb</groupId>
<artifactId>r2dbc-mariadb</artifactId>
<version>${r2dbc-mariadb.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-spi-test</artifactId>
@@ -256,6 +274,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mariadb</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>

View File

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

View File

@@ -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<UUID, String> {
INSTANCE;
@Override
public String convert(UUID uuid) {
return uuid.toString();
}
}
@ReadingConverter
enum StringToUuidConverter implements Converter<String, UUID> {
INSTANCE;
@Override
public UUID convert(String value) {
return UUID.fromString(value);
}
}
}

View File

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

View File

@@ -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<? extends LegoSetRepository> getRepositoryInterfaceType() {
return MySqlLegoSetRepository.class;
}
interface MySqlLegoSetRepository extends LegoSetRepository {
@Override
@Query("SELECT * FROM legoset WHERE name like ?")
Flux<LegoSet> findByNameContains(String name);
@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,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<ExternalDatabase>... 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;
}
}