Setting Up a Local MariaDB Using Testcontainers in Spring Boot

Truong Bui
7 min readMay 29, 2023

--

Part of the Testcontainers Article Series: If you haven’t read my other articles yet, please refer to the following links:
1. Setting Up a Local Kafka Using Testcontainers in Spring Boot
2. Setting Up a Local Redis Cluster Using Testcontainers in Spring Boot
3. Setting Up a Local Jira Instance Using Testcontainers in Spring Boot

During project development, it is common to encounter three key environments: Development (Dev), Staging (Stg), and Production (Prod).

Consider a scenario where your application needs a database connection in all three environments. In the Staging (Stg) and Production (Prod) environments, there are dedicated database servers, making the connections straightforward. However, what about the Development (Dev) environment? It can say that having a database server in Dev is optional.

Having a dedicated database instance for the Development (Dev) environment would be fantastic 😎. However, if that’s not the case, it can lead to challenges when running the application locally. For example:

  1. You must pre-configure the database by either installing it on your personal machine or utilizing a Docker image. Make sure to run the database in advance whenever you launch the application locally.
  2. Alternatively, you would need to connect to the Staging (Stg) database, which might be inconvenient in certain scenarios. This setup can cause issues, such as unintended impacts on the Quality Assurance (QA) team testing the app on Stg or the database server running out of resources due to excessive database operations, etc.

Having encountered a similar situation in the past, I have successfully tackled this issue using Testcontainers. By leveraging Testcontainers, we can overcome the challenges mentioned earlier. Today, I will create a small project to showcase the setup of a local database using Testcontainers. (I won’t delve deeply into Testcontainers concepts, but I will make an effort to provide thorough explanations at each step and include official documentation links for reference)

Now let’s get started!!! 💪

Prerequisites

  • Java 17
  • Maven Wrapper
  • Spring Boot 3+
  • Swagger (for testing purposes)
  • Docker runtime in advance (Docker Install)

Define Dependencies

Create the project as a Spring Boot project with the dependencies provided inside below POM file. I have named it mariadb.

https://github.com/buingoctruong/springboot-testcontainers-database/blob/master/pom.xml

Database Properties

In order to connect to the MariaDB database, we need to add corresponding properties to application.yaml file.

mariadb:
# URL connection to database (appDB is database name)
jdbc-url: jdbc:mariadb://localhost:3306/appDB
# MariaDB driver dependency
driver-class-name: org.mariadb.jdbc.Driver
# Maximum pool size
maximum-pool-size: 1
username: admin
password: password

The full version of the application.yaml file can be found here: https://github.com/buingoctruong/springboot-testcontainers-database/blob/master/src/main/resources/application.yaml

Entity

Let’s create our Celebrity class, keeping the model simple for this project.

@Data
@Entity
@Table(name = "celebrity")
public class Celebrity {
@Id
private int celebrityId;
private int age;
private String name;
}

You can enhance constraints on fields using Jakarta Validation Constraints

Repository

public interface CelebrityRepository extends JpaRepository<Celebrity, Integer> {
Optional<Celebrity> findById(Integer celebrityId);
}

Controller

A simple controller to test the local machine’s MariaDB connection.

  • /mariadb/{id}: Retrieve a particular celebrity by their id.
  • /mariadb/pageable: Retrieve celebrities per page.
@RestController
@RequestMapping("/mariadb")
@RequiredArgsConstructor
public class MariaDBController {
private final CelebrityRepository celebrityRepository;
@GetMapping("/{id}")
public Celebrity getByCelebrityId(@PathVariable("id") int id) {
return celebrityRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Celebrity Not Found: " + id));
}

@GetMapping(path = "/pageable")
public Page<Celebrity> getPage(@ParameterObject @PageableDefault(size = 20,
sort = "celebrityId", direction = Sort.Direction.ASC) Pageable pageable) {
return celebrityRepository.findAll(pageable);
}
}

Data Access Configuration

We utilize certain annotations in our data access configuration class. Official documentation links are provided for further reference.

Inside the configuration class, there are a few Bean methods that require definition. Let’s delve into them in more detail.

DataSource

@Bean
@ConfigurationProperties(prefix = "mariadb")
public HikariDataSource mariadbDataSource() {
return new HikariDataSource();
}

While Spring Boot’s automatic DataSource configuration works very well in most cases, however we need a higher level of control, so we’ll have to set up our own DataSource implementation.

We utilize HikariCP’s DataSource to create a single instance of a data source for our application. By annotating this @Bean method with @ConfigurationProperties, we can bind properties from an external source, such as the application.yaml file.

For detailed information on configuring multiple data sources, please refer to this resource: Configure Two DataSources

EntityManagerFactory

@Bean
public LocalContainerEntityManagerFactoryBean mariadbEntityManager(
EntityManagerFactoryBuilder entityManagerFactoryBuilder,
HikariDataSource mariadbDataSource) {
Map<String, String> jpaProperties = new HashMap<>();
jpaProperties.put("database-platform", "org.hibernate.dialect.MariaDB10Dialect");
return entityManagerFactoryBuilder.dataSource(mariadbDataSource)
.packages("io.github.truongbn.mariadb.entity").persistenceUnit("mariadbDataSource")
.properties(jpaProperties).build();
}

To use JPA, the setup of an EntityManagerFactory is essential as it manages entity persistence within the application. LocalContainerEntityManagerFactoryBean offers a powerful way for establishing a EntityManagerFactory.

For additional information, please refer to: LocalContainerEntityManagerFactoryBean and EntityManagerFactoryBuilder.

TransactionManagers

@Bean
public PlatformTransactionManager mariadbTransactionManager(
EntityManagerFactory mariadbEntityManager) {
return new JpaTransactionManager(mariadbEntityManager);
}

To complete the picture, we need to configure TransactionManagers for the EntityManagerFactory. TransactionManagers is crucial for managing database transactions, ensuring the proper handling of transactional operations like committing or rolling back changes to the database.

Local Database Setup

Here we go! The most interesting section 😃

As you may have noticed, the value of “mariadb.jdbc-url” in the application.yaml file is merely a placeholder. There is no actual database exists with the URL connection “jdbc:mariadb://localhost:3306/appDB”.

During application startup, the following steps need to be taken:

  1. Utilize Testcontainers for constructing a container implementation for MariaDB.
  2. Starts the container using docker, pulling an image if necessary.
  3. Retrieve the URL connection of the recently started container.
  4. Update the value of “mariadb.jdbc-url” in the application.yaml file.

Now, the question arises: How can we execute code during application startup? The solution is to implement ApplicationContextInitializer interface, which accepts ApplicationContextInitializedEvent. This event is sent when ApplicationContext becomes available, but before any bean definitions are loaded.

@Configuration
public class LocalMariaDBInitializer
implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext context) {
mariaDbLocalSetup(context);
}

private void mariaDbLocalSetup(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
DockerImageName dockerImageName = DockerImageName.parse("mariadb:10.5.5");
MariaDBContainer<?> mariaDB = new MariaDBContainer<>(dockerImageName)
.withDatabaseName("appDB").withUsername("admin").withPassword("password")
.withInitScript("MariaDbLocalSetupScript.sql");
mariaDB.start();
String jdbcUrl = mariaDB.getJdbcUrl();
setProperties(environment, "mariadb.jdbc-url", jdbcUrl);
}

private void setProperties(ConfigurableEnvironment environment, String name, Object value) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get(name);
if (source == null) {
source = new MapPropertySource(name, new HashMap<>());
sources.addFirst(source);
}
((Map<String, Object>) source.getSource()).put(name, value);
}
}

I will assign the exploration of constructing container implementations using Testcontainers and the ApplicationContextInitializer interface as homework for you. 😃

The “MariaDbLocalSetupScript.sql” script is used for setting up the database table. You can find it at this link: https://github.com/buingoctruong/springboot-testcontainers-database/blob/master/src/test/resources/MariaDbLocalSetupScript.sql

Testcontainers should only be utilized in the local environment. This configuration class is created to facilitate local application execution with a database. Therefore, it should be moved to the test folder.

Local Application Startup Class

To start up the Spring ApplicationContext, we need a Spring Boot application’s main class that contains a public static void main() method.

Inside the test folder, there exists a class named “MariadbApplicationTests”. I have renamed it to “MariadbAppRunner” 😆 and made the following updates.

@SpringBootTest
@ComponentScan(basePackages = "io.github.truongbn.mariadb")
@ConfigurationPropertiesScan(basePackages = "io.github.truongbn.mariadb")
class MariadbAppRunner {
public static void main(String[] args) {
new SpringApplicationBuilder(MariadbAppRunner.class)
.initializers(new LocalMariaDBInitializer()).run(args);
}
}

Time to play with Testcontainers

Now, everything is ready! 😎

To launch the application, run MariadbAppRunner.main() method, it should run successfully on port 8080.

The initial run may take some time as Testcontainers needs to set up all the necessary components for the docker instance. 😅 However, this setup process only occurs once. The positive side is that from now on, we can launch the application locally at any time without the need for manual configuration of database-related tasks.

If you encounter this issue during your initial run, and you find yourself in a similar situation as I did, you can refer to this link for more details: https://github.com/testcontainers/testcontainers-java/discussions/6045

  • Try out with “/mariadb/2”, and the expected result should be as follows

I will assign the task of trying out “/mariadb/pageable” as homework for you. 😆

We have just completed a brief demonstration to observe the setup of a local database using Testcontainers. Additionally, Testcontainers can be utilized for writing integration tests. Isn’t amazing? 😃 hope it’s working as expected you guys!

Completed source code can be found in this GitHub repository: https://github.com/buingoctruong/springboot-testcontainers-database

I would love to hear your thoughts!

Thank you for reading, and goodbye!

--

--