or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

container-configuration.mdcontainer-customization.mdcontainer-lifecycle.mddatabase-initialization.mdindex.mdjdbc-connection.mdr2dbc-support.mdurl-parameters-timeouts.md
tile.json

r2dbc-support.mddocs/

R2DBC Reactive Support

This document covers using MySQL containers with R2DBC for reactive, non-blocking database access with Project Reactor or other reactive frameworks.

Quick Reference

R2DBC Classes:

  • MySQLR2DBCDatabaseContainer - Wrapper that adapts MySQLContainer for R2DBC
  • MySQLR2DBCDatabaseContainerProvider - Provider for URL-based R2DBC container creation

Key Methods:

  • MySQLR2DBCDatabaseContainer(MySQLContainer<?> container) - Constructor
  • MySQLR2DBCDatabaseContainer.getOptions(MySQLContainer<?> container): ConnectionFactoryOptions - Static method to get ConnectionFactoryOptions
  • configure(ConnectionFactoryOptions options): ConnectionFactoryOptions - Configure options from container
  • start(): void / stop(): void - Lifecycle methods (delegated to wrapped container)

Required Dependencies:

  • org.testcontainers:mysql:1.21.4 (this package)
  • org.testcontainers:r2dbc:1.21.4 (R2DBC support)
  • io.asyncer:r2dbc-mysql:1.0.0 (R2DBC MySQL driver)

Important Notes:

  • R2DBC operations are asynchronous and non-blocking
  • Supports backpressure through Reactive Streams
  • Connection factory is thread-safe
  • Must start underlying MySQLContainer before using R2DBC wrapper

Overview

The MySQL module provides full R2DBC (Reactive Relational Database Connectivity) support, allowing you to use MySQL containers with reactive applications. R2DBC enables asynchronous, non-blocking database operations with backpressure support through the Reactive Streams specification.

Dependencies

To use R2DBC support, add these dependencies to your project:

Gradle:

testImplementation 'org.testcontainers:mysql:1.21.4'
testImplementation 'org.testcontainers:r2dbc:1.21.4'
testImplementation 'io.asyncer:r2dbc-mysql:1.0.0'

Maven:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.21.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>r2dbc</artifactId>
    <version>1.21.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.asyncer</groupId>
    <artifactId>r2dbc-mysql</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

Capabilities

MySQLR2DBCDatabaseContainer

Wrapper class that adapts a MySQLContainer for R2DBC usage.

/**
 * Creates an R2DBC database container wrapper around an existing MySQLContainer.
 * Delegates lifecycle management (start, stop) to the wrapped container.
 * The wrapped MySQLContainer must be started before using R2DBC operations.
 *
 * @param container the MySQLContainer to wrap (must not be null)
 */
class MySQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer {
    MySQLR2DBCDatabaseContainer(MySQLContainer<?> container);

    /**
     * Creates R2DBC ConnectionFactoryOptions from a MySQLContainer.
     * This is a convenience method that creates a wrapper and configures options.
     * The container must be started before calling this method.
     *
     * @param container the MySQLContainer to get options from (must not be null, must be started)
     * @return ConnectionFactoryOptions configured for the container
     * @throws IllegalStateException if container is not started
     */
    static ConnectionFactoryOptions getOptions(MySQLContainer<?> container);

    /**
     * Configures R2DBC ConnectionFactoryOptions with container connection details.
     * Sets host, port, database name, username, and password from the container.
     * The container must be started before calling this method.
     *
     * @param options the base ConnectionFactoryOptions to configure
     * @return configured ConnectionFactoryOptions
     * @throws IllegalStateException if wrapped container is not started
     */
    ConnectionFactoryOptions configure(ConnectionFactoryOptions options);

    /**
     * Starts the wrapped MySQL container.
     * Delegated to the wrapped MySQLContainer.start().
     *
     * @throws ContainerLaunchException if container fails to start
     * @throws IllegalStateException if container cannot be accessed within timeout
     */
    void start();

    /**
     * Stops the wrapped MySQL container.
     * Delegated to the wrapped MySQLContainer.stop().
     */
    void stop();
}

Usage Example:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;

// Create MySQL container
MySQLContainer<?> mysqlContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
    .withDatabaseName("testdb");

// Wrap for R2DBC usage
MySQLR2DBCDatabaseContainer r2dbcContainer = new MySQLR2DBCDatabaseContainer(mysqlContainer);
r2dbcContainer.start();

// Get R2DBC connection options
ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysqlContainer);

// Create R2DBC ConnectionFactory
ConnectionFactory connectionFactory = ConnectionFactories.get(options);

// Use with reactive code...

r2dbcContainer.stop();

MySQLR2DBCDatabaseContainerProvider

Factory class for creating R2DBC MySQL containers from ConnectionFactoryOptions.

class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider {
    /**
     * The R2DBC driver identifier for MySQL.
     * Value: "mysql" (from MySqlConnectionFactoryProvider.MYSQL_DRIVER)
     */
    static final String DRIVER = "mysql";

    /**
     * Checks if this provider supports the given connection options.
     * Returns true if the driver option is "mysql".
     *
     * @param options the ConnectionFactoryOptions to check
     * @return true if driver is "mysql", false otherwise
     */
    boolean supports(ConnectionFactoryOptions options);

    /**
     * Creates an R2DBC database container from connection options.
     * Extracts image tag, database name, and reuse settings from options.
     *
     * @param options the ConnectionFactoryOptions specifying container configuration
     * @return an R2DBCDatabaseContainer instance
     * @throws IllegalArgumentException if options are invalid
     */
    R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options);

    /**
     * Returns connection factory metadata with default credentials if not specified.
     * Adds default username "test" and password "test" if not present in options.
     *
     * @param options the ConnectionFactoryOptions to get metadata for
     * @return ConnectionFactoryMetadata with defaults applied, or null
     */
    ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options);
}

Usage Example:

import org.testcontainers.containers.MySQLR2DBCDatabaseContainerProvider;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;

// Create options for container creation
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
    .option(ConnectionFactoryOptions.DRIVER, "tc")
    .option(Option.valueOf("tc-type"), "mysql")
    .option(ConnectionFactoryOptions.DATABASE, "testdb")
    .build();

// Provider is automatically discovered and used by ConnectionFactories

Complete R2DBC Usage Examples

Basic R2DBC with Project Reactor

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class R2DBCExample {

    public void testReactiveDatabase() {
        // Create and start container
        MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
            .withDatabaseName("reactivedb")
            .withInitScript("schema.sql");

        MySQLR2DBCDatabaseContainer r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
        r2dbc.start();

        try {
            // Get connection factory
            ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);
            ConnectionFactory connectionFactory = ConnectionFactories.get(options);

            // Execute reactive queries
            Flux<Integer> results = Flux.usingWhen(
                connectionFactory.create(),
                connection -> Flux.from(connection
                    .createStatement("SELECT id FROM users WHERE active = ?")
                    .bind(0, true)
                    .execute())
                    .flatMap(result -> result.map((row, metadata) ->
                        row.get("id", Integer.class)
                    )),
                connection -> connection.close()
            );

            // Process results reactively
            results.subscribe(id -> System.out.println("User ID: " + id));

        } finally {
            r2dbc.stop();
        }
    }
}

R2DBC with Spring Data R2DBC

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactoryOptions;

@SpringBootTest
@Testcontainers
public class SpringR2DBCTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
        .withDatabaseName("springtest");

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) {
        ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);

        registry.add("spring.r2dbc.url", () ->
            String.format("r2dbc:mysql://%s:%d/%s",
                mysql.getHost(),
                mysql.getMappedPort(MySQLContainer.MYSQL_PORT),
                mysql.getDatabaseName()
            )
        );
        registry.add("spring.r2dbc.username", mysql::getUsername);
        registry.add("spring.r2dbc.password", mysql::getPassword);
    }

    // Spring R2DBC repositories and tests...
}

Manual R2DBC Connection Configuration

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.core.publisher.Mono;

public void manualR2DBCConfiguration() {
    MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
        .withDatabaseName("manualdb");
    mysql.start();

    try {
        // Manually build R2DBC connection options
        ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
            .option(ConnectionFactoryOptions.DRIVER, "mysql")
            .option(ConnectionFactoryOptions.HOST, mysql.getHost())
            .option(ConnectionFactoryOptions.PORT, mysql.getMappedPort(MySQLContainer.MYSQL_PORT))
            .option(ConnectionFactoryOptions.DATABASE, mysql.getDatabaseName())
            .option(ConnectionFactoryOptions.USER, mysql.getUsername())
            .option(ConnectionFactoryOptions.PASSWORD, mysql.getPassword())
            .build();

        ConnectionFactory connectionFactory = ConnectionFactories.get(options);

        // Use connection factory
        Mono.from(connectionFactory.create())
            .flatMapMany(connection ->
                Mono.from(connection.createStatement("SELECT 1").execute())
                    .flatMapMany(result -> result.map((row, metadata) -> row.get(0, Integer.class)))
                    .doFinally(signalType -> connection.close())
            )
            .subscribe(value -> System.out.println("Result: " + value));

    } finally {
        mysql.stop();
    }
}

JUnit 5 Integration with R2DBC

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.test.StepVerifier;
import reactor.core.publisher.Flux;

public class R2DBCJUnit5Test {

    private static MySQLContainer<?> mysql;
    private static MySQLR2DBCDatabaseContainer r2dbc;
    private static ConnectionFactory connectionFactory;

    @BeforeAll
    static void setUp() {
        mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
            .withDatabaseName("junit5test");

        r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
        r2dbc.start();

        ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);
        connectionFactory = ConnectionFactories.get(options);
    }

    @AfterAll
    static void tearDown() {
        r2dbc.stop();
    }

    @Test
    void testReactiveQuery() {
        Flux.usingWhen(
            connectionFactory.create(),
            connection -> Flux.from(connection
                .createStatement("SELECT 1 as value")
                .execute())
                .flatMap(result -> result.map((row, metadata) ->
                    row.get("value", Integer.class)
                )),
            connection -> connection.close()
        )
        .as(StepVerifier::create)
        .expectNext(1)
        .verifyComplete();
    }
}

R2DBC Transaction Example

import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;

public void transactionalOperation(ConnectionFactory connectionFactory) {
    Mono.usingWhen(
        connectionFactory.create(),
        connection -> Mono.from(connection.beginTransaction())
            .then(Mono.from(connection.createStatement(
                "INSERT INTO accounts (name, balance) VALUES (?, ?)")
                .bind(0, "Alice")
                .bind(1, 1000)
                .execute()))
            .then(Mono.from(connection.createStatement(
                "INSERT INTO accounts (name, balance) VALUES (?, ?)")
                .bind(0, "Bob")
                .bind(1, 500)
                .execute()))
            .then(Mono.from(connection.commitTransaction()))
            .onErrorResume(error ->
                Mono.from(connection.rollbackTransaction())
                    .then(Mono.error(error))
            ),
        Connection::close
    )
    .subscribe();
}

Container Reuse with R2DBC

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;

// Enable container reuse for faster test execution
MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
    .withDatabaseName("reusabledb")
    .withReuse(true); // Enable reuse

MySQLR2DBCDatabaseContainer r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
r2dbc.start();

// Container will be reused across test runs

R2DBC URL Format

R2DBC URLs for MySQL follow this format:

r2dbc:mysql://[host]:[port]/[database]?[options]

Example:

r2dbc:mysql://localhost:32768/testdb

The MySQLR2DBCDatabaseContainer handles URL construction automatically based on container settings.

Best Practices

  1. Use ConnectionFactoryOptions.getOptions(): Simplifies configuration by extracting all settings from the container
  2. Leverage Reactive Patterns: Use Flux/Mono for backpressure-aware database operations
  3. Manage Connections Properly: Use usingWhen to ensure connections are closed
  4. Handle Transactions Correctly: Always rollback on error to maintain database consistency
  5. Test with Real Backpressure: Use Project Reactor's test utilities (StepVerifier) for thorough testing
  6. Enable Container Reuse: Speed up test execution in development
  7. Configure Timeouts: Increase startup/connect timeouts for CI environments
  8. Use Connection Pooling: Consider r2dbc-pool for production-like testing
  9. Start Container Before Use: Ensure MySQLContainer is started before creating R2DBC wrapper
  10. Handle Errors Reactively: Use reactive error handling operators (onErrorResume, onErrorReturn)

Troubleshooting

R2DBC Driver Not Found: Ensure io.asyncer:r2dbc-mysql is on your classpath

Connection Timeout: Increase container startup timeout:

mysql.withStartupTimeoutSeconds(180);

Port Mapping Issues: Ensure you're using the mapped port from the container:

mysql.getMappedPort(MySQLContainer.MYSQL_PORT)

Transaction Issues: Verify you're properly committing/rolling back transactions in reactive flows

Backpressure Issues: Use Project Reactor's backpressure operators (onBackpressureBuffer, onBackpressureDrop) when needed

IllegalStateException: Ensure container is started before calling getOptions() or configure()