Skip to content

Replace h2 #4291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions articles/building-apps/forms-data/add-flyway.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,6 @@ Now <<{articles}/getting-started/run#,run>> the application. You should see simi
[%collapsible]
====
You've now implemented Flyway for schema management in a Vaadin application and replaced Hibernate's DDL auto-generation with a structured, version-controlled approach.
====

// TODO add link to article about replacing H2 with PostgreSQL once it's written
Next, you should consider replacing H2 with a better database, such as PostgreSQL. For details, see the <<replace-h2#,Replace H2>> guide.
====
383 changes: 383 additions & 0 deletions articles/building-apps/forms-data/replace-h2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
---
title: Replace H2
page-title: How to replace H2 with PostgreSQL | Vaadin
description: Learn how to replace the H2 database with PostgreSQL.
meta-description: Learn how to replace the H2 database with PostgreSQL in your Vaadin application, using Testcontainers and proper configuration for development and testing.
order: 40
---


= Replace H2
:toclevels: 2

Many Spring Boot applications start with H2 because it's lightweight and easy to configure. For instance, Vaadin's <</getting-started/walk-through#,Walking Skeleton>> uses H2. However, you typically don't run H2 in production. Switching to your production database early in development helps catch compatibility issues sooner and lets you leverage database-specific features for performance.

This guide teaches you how to replace H2 with PostgreSQL, although the same principle can be applied to other databases such as MySQL, Oracle, and Microsoft SQL Server. A hands-on mini-tutorial at the end helps you apply these concepts in a real Vaadin application.


== Replace Maven Dependencies

Start by removing the H2 dependency from your Maven `pom.xml`, and replace it with the appropriate driver for your production database. For PostgreSQL, you should replace it with this:

[source,xml]
----
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
----

If you are using <<add-flyway#,Flyway migrations>> -- which you should at this point -- you also need to add the correct Flyway database dependency:

[source,xml]
----
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
----

This is needed because Flyway requires a separate module to understand how to handle vendor-specific features in PostgreSQL.


== Add Testcontainers

For integration tests, H2 started up an in-memory database and used it for the tests. Existing integration tests will likely fail after replacing H2, because they no longer have a database to connect to. To fix this, you can use https://testcontainers.com/[Testcontainers].

Check warning on line 47 in articles/building-apps/forms-data/replace-h2.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.Will] Avoid using 'will'. Raw Output: {"message": "[Vaadin.Will] Avoid using 'will'.", "location": {"path": "articles/building-apps/forms-data/replace-h2.adoc", "range": {"start": {"line": 47, "column": 114}}}, "severity": "WARNING"}

Testcontainers is an open-source library that provides temporary, disposable instances of databases and other services in Docker containers. You can configure your integration tests to start up a new database instance, connect to it, run the tests, and then throw it away.

.Docker required
[IMPORTANT]
Testcontainers requires Docker, so if you don't have it on your machine you should install it now. Ensure that Docker is installed and running before executing your tests.


=== Add Maven Dependencies

Spring Boot includes Testcontainers in its parent POM, so you don't have to manage the version yourself. You have to add the dependencies to your POM-file, though:

[source,xml]
----
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
----


=== Update Integration Tests

To use Testcontainers in your tests, you need to:

1. Start the database Docker container before you initialize the test.
2. Configure Spring to connect to the database container.
3. Stop the container after the test is finished.

[source,java]
----
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyDatabaseIT {

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:17-alpine" // <1>
);

@BeforeAll
static void beforeAll() { // <2>
postgres.start();
}

@AfterAll
static void afterAll() { // <3>
postgres.stop();
}

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) { // <4>
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}

// (Test methods)
}
----
<1> This is the name of the PostgreSQL Docker image you want to use.
<2> Starts PostgreSQL before running any tests in the test class.
<3> Stops PostgreSQL after running all the tests in the test class.
<4> Updates Spring's configuration to connect to the PostgreSQL instance.

==== Abstract Base Class

If you have many integration tests, you can move the container configuration to an abstract base class, like this:

.AbstractIntegrationTest.java
[source,java]
----
public abstract class AbstractIntegrationTest {

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:17-alpine"
);

@BeforeAll
static void beforeAll() {
postgres.start();
}

@AfterAll
static void afterAll() {
postgres.stop();
}

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
----

Your integration tests then become quite simple:

[source,java]
----
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyDatabaseIT extends AbstractIntegrationTest {
// (Test methods)
}
----


== Start a Development Database

Once integration tests are passing, you'll also want to run the application using a local PostgreSQL instance. You can do this with Docker:

[source,terminal]
----
docker run --name my-development-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres:17-alpine
----

Unlike the in-memory H2 database, this development database persists between application restarts. When the database schema has stabilized, this is desired as you don't have to initialize the database every time the application starts up. However, there may still be cases when you need to reset the database. You can do this by stopping and deleting the Docker container:

[source,terminal]
----
docker stop my-development-postgres
docker rm my-development-postgres
----

After that, re-create the development database and restart your application.


== Update Application Configuration

The final step before you can run your application is to configure it to connect to your development database. You typically do this in the `src/main/resources/application.properties` file. Because this file is typically checked into source control, *it should not contain sensitive credentials*. The file should also *not contain anything that is dangerous to use in production*, like configuring Hibernate to drop and re-create the database.

The credentials of the local development database should never be used anywhere else than on the local machine. Therefore they can be checked into source control. Also, if the application accidentally starts up with them in production, it can't do any harm since the production database would use different credentials (and probably a different URL).

In production, the real credentials would come from a different configuration file or a vault. Because of this, you can use `${..}` placeholders for the real credentials, and use the local development credentials as default values. For production, use Spring profiles or external configuration sources to override these default values:

.application.properties
[source]
----
spring.datasource.url=${secrets.datasource.url:jdbc:postgresql://localhost/postgres}
spring.datasource.username=${secrets.datasource.username:postgres}
spring.datasource.password=${secrets.datasource.password:mysecretpassword}
----

In the example above, Spring would read the real database username from the `secrets.datasource.username` property. If that property does not exist, it reverts to `postgres`. The same pattern is used for the other properties.


=== Update Flyway Configuration

In production, it is good practice to use separate database user accounts for Data Definition Language (DDL) and Data Modification Language (DML) queries. In practice, this means Flyway should use a different account than the rest of the application. However, in development, it is often easier to use the same account for both. Again, you can use `${..}` placeholders to achieve this:

.application.properties
[source]
----
spring.flyway.user=${secrets.flyway.user:${spring.datasource.username}}
spring.flyway.password=${secrets.flyway.password:${spring.datasource.password}}
----

In this example, Spring would read the Flyway database username from the `secrets.flyway.user` property. If that property does not exist, it reverts to `spring.datasource.username`.


[.collapsible-list]
== Try It

In this tutorial, you'll replace H2 with PostgreSQL in a real Vaadin application.

.Set Up the Project
[%collapsible]
====
Use the same project from the <<add-flyway#,Add Flyway>> mini-tutorial. Complete that tutorial before proceeding with this one.
====

.Update Database Dependencies
[%collapsible]
====
In `pom.xml`, locate the H2 dependency:

[source,xml]
----
<dependency>
<!-- Replace with the database you will be using in production -->
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
----

Replace it with the PostgreSQL dependency:

[source,xml]
----
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
----

Also add the Flyway PostgreSQL dependency:

[source,xml]
----
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
----
====

.Add Testcontainers Dependency
[%collapsible]
====
Still in `pom.xml`, add the following test dependencies:

[source,xml]
----
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
----
====

.Create Integration Test Base Class
[%collapsible]
====
In `[application package]`, create a new `AbstractIntegrationTest` class:

.AbstractIntegrationTest.java
[source,java]
----
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

public abstract class AbstractIntegrationTest {

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
"postgres:17-alpine"
);

@BeforeAll
static void beforeAll() {
postgres.start();
}

@AfterAll
static void afterAll() {
postgres.stop();
}

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
----
====

.Update Integration Test
[%collapsible]
====
Open `TaskServiceIT` and change it to extend the base class:

.TaskServiceIT.java
[source,java]
----
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
// tag::snippet[]
class TaskServiceIT extends AbstractIntegrationTest {
// end::snippet[]
// (tests omitted)
}
----

Now run the integration test. Remember that you must have Docker running for everything to work.
====

.Start Development Database
[%collapsible]
====
Open a terminal and run the following command:

[source,terminal]
----
docker run --name my-development-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres:17-alpine
----

If you already have PostgreSQL running on your machine, this won't work as port 5432 is already in use. If port 5432 is in use, map it to a different host port -- such as `-p 5433:5432` -— to avoid conflicts.
====

.Update Application Configuration
[%collapsible]
====
Open `application.properties` and add the following lines:

[source]
----
spring.datasource.url=${secrets.datasource.url:jdbc:postgresql://localhost/postgres}
spring.datasource.username=${secrets.datasource.username:postgres}
spring.datasource.password=${secrets.datasource.password:mysecretpassword}
spring.flyway.user=${secrets.flyway.user:${spring.datasource.username}}
spring.flyway.password=${secrets.flyway.password:${spring.datasource.password}}
----

If you mapped PostgreSQL to a different port than 5432, you have to update the URL accordingly (e.g., `jdbc:postgresql://localhost:5433/postgres`).
====

.Test the Application
[%collapsible]
====
Now <<{articles}/getting-started/run#,run>> the application. It should start up normally. Add some tasks, then restart the application. The tasks should still be there.
====

.Final Thoughts
[%collapsible]
====
You've now replaced the H2 database with PostgreSQL in a Vaadin application. In a real-world application, review your existing Flyway migrations to ensure all SQL statements are compatible with PostgreSQL.
====
Loading