Spring BootH2 Database
Spring Boot

H2 Database

H2 is a lightweight, pure-Java relational database that runs embedded inside the JVM or as a standalone server. In Spring Boot it is the standard choice for development and testing — it requires zero installation, starts in milliseconds, and auto-configures with a single dependency. The H2 console provides a browser-based SQL interface for inspecting data during development.

Setup and Auto-Configuration

Adding the H2 dependency with test or runtime scope triggers Spring Boot's EmbeddedDatabaseAutoConfiguration. An in-memory H2 database is created automatically with no datasource URL required. Spring Boot also auto-configures the H2 console when the web dependency is present.
XML
<!-- pom.xml — H2 for development (runtime scope) and testing: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Runtime scope — available in dev and test but not in production JAR: -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

# ── application.yml — H2 auto-configures with no datasource needed: ───
# Spring Boot detects H2 on the classpath and creates an in-memory DB.
# The DB name is random by default — use a fixed name to share between
# connections (e.g. the H2 console and your application):

spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    # DB_CLOSE_DELAY=-1  — keep DB alive as long as JVM runs
    # DB_CLOSE_ON_EXIT=FALSE — don't close when last connection closes
    driver-class-name: org.h2.Driver
    username: sa
    password: ""

  jpa:
    hibernate:
      ddl-auto: create-drop     # create schema on start, drop on stop
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect
        format_sql: true

  h2:
    console:
      enabled: true             # enable browser-based SQL console
      path: /h2-console         # URL: http://localhost:8080/h2-console
      settings:
        trace: false
        web-allow-others: false # allow only localhost (security)

H2 Modes — Compatibility with Production Databases

H2 supports compatibility modes that mimic the behaviour of MySQL, PostgreSQL, Oracle, and SQL Server. Enabling compatibility mode means the same SQL that runs in production also runs in H2 during testing.
yaml
# ── MySQL compatibility mode: ─────────────────────────────────────────
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
    # Enables: AUTO_INCREMENT, IFNULL, GROUP BY behaviour matching MySQL

# ── PostgreSQL compatibility mode: ────────────────────────────────────
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1
    # DATABASE_TO_LOWER=TRUE — H2 is case-insensitive by default;
    # PostgreSQL folds identifiers to lowercase — this matches that behaviour

# ── Oracle compatibility mode: ────────────────────────────────────────
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=Oracle;DB_CLOSE_DELAY=-1

# ── SQL Server compatibility mode: ────────────────────────────────────
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MSSQLServer;DB_CLOSE_DELAY=-1

# ── Compatibility mode limitations: ───────────────────────────────────
# H2 compatibility is not 100% — some advanced features differ:
# - PostgreSQL: arrays, JSONB, LATERAL joins not fully supported
# - MySQL: ON DUPLICATE KEY UPDATE partially supported
# - Oracle: CONNECT BY, MERGE differences
# For comprehensive testing of DB-specific features: use Testcontainers.

# ── File-based H2 — data persists across restarts: ────────────────────
spring:
  datasource:
    url: jdbc:h2:file:./data/testdb;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE
    # AUTO_SERVER=TRUE — allows multiple connections to the same file DB
    # Useful when the app and the H2 console connect simultaneously

H2 Console

The H2 console is a browser-based SQL interface for inspecting and querying the in-memory database during development. It is only available when spring.h2.console.enabled = true and the web dependency is on the classpath.
yaml
# ── Enable the H2 console: ────────────────────────────────────────────
spring:
  h2:
    console:
      enabled: true
      path: /h2-console

# ── Access: http://localhost:8080/h2-console ──────────────────────────
# Connection settings in the H2 console login page:
# Driver Class:  org.h2.Driver
# JDBC URL:      jdbc:h2:mem:testdb    (must match spring.datasource.url)
# User Name:     sa
# Password:      (empty)

# ── Spring Security — allow H2 console when security is on: ───────────
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/h2-console/**").permitAll()  // allow console
                .anyRequest().authenticated()
            )
            // H2 console uses iframes — must disable frameOptions:
            .headers(headers -> headers
                .frameOptions(frame -> frame.sameOrigin()))
            // H2 console submits forms — disable CSRF for console path:
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/h2-console/**"));
        return http.build();
    }
}

# ── application-dev.yml (dev profile only): ──────────────────────────
spring:
  h2:
    console:
      enabled: true   # never enable in prod — only in dev profile

Schema and Data Initialisation

Spring Boot can initialise the H2 schema and seed data from SQL scripts. schema.sql creates the tables; data.sql inserts reference data. These run automatically when H2 is the embedded database.
yaml
# ── application.yml — enable SQL initialisation: ─────────────────────
spring:
  sql:
    init:
      mode: always          # always, embedded (default), or never
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql
      encoding: UTF-8
  jpa:
    hibernate:
      ddl-auto: none        # let schema.sql manage DDL, not Hibernate

# ── src/main/resources/schema.sql: ────────────────────────────────────
CREATE TABLE IF NOT EXISTS users (
    id          BIGINT          NOT NULL AUTO_INCREMENT,
    name        VARCHAR(100)    NOT NULL,
    email       VARCHAR(255)    NOT NULL UNIQUE,
    role        VARCHAR(20)     NOT NULL DEFAULT 'USER',
    active      BOOLEAN         NOT NULL DEFAULT TRUE,
    created_at  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

CREATE TABLE IF NOT EXISTS orders (
    id          BIGINT          NOT NULL AUTO_INCREMENT,
    user_id     BIGINT          NOT NULL,
    reference   VARCHAR(50)     NOT NULL UNIQUE,
    total       DECIMAL(10,2)   NOT NULL,
    created_at  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

# ── src/main/resources/data.sql: ──────────────────────────────────────
INSERT INTO users (name, email, role) VALUES
    ('Alice Smith',   'alice@example.com',   'ADMIN'),
    ('Bob Jones',     'bob@example.com',     'USER'),
    ('Carol White',   'carol@example.com',   'USER');

INSERT INTO orders (user_id, reference, total) VALUES
    (1, 'ORD-001', 99.99),
    (1, 'ORD-002', 149.50),
    (2, 'ORD-003', 29.99);

# ── With Flyway and H2 — use Flyway scripts instead: ─────────────────
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration  # same migrations as production
  sql:
    init:
      mode: never   # Flyway handles schema — disable Spring's init

H2 in Tests

H2 is the standard in-memory database for unit and slice tests. Spring Boot's @DataJpaTest auto-configures H2, Hibernate, and Spring Data repositories in isolation — no web layer, no service beans, minimal context.
Java
// ── @DataJpaTest — lightweight JPA slice test with H2: ───────────────
@DataJpaTest   // configures H2, JPA, repositories; excludes web layer
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;   // for arranging test data

    @Test
    void findByEmail_returnsUser_whenExists() {
        // Arrange — persist using TestEntityManager:
        User user = new User("Alice", "alice@example.com");
        entityManager.persistAndFlush(user);

        // Act:
        Optional<User> found = userRepository.findByEmail("alice@example.com");

        // Assert:
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Alice");
    }

    @Test
    void existsByEmail_returnsTrue_whenExists() {
        entityManager.persistAndFlush(new User("Bob", "bob@example.com"));
        assertThat(userRepository.existsByEmail("bob@example.com")).isTrue();
        assertThat(userRepository.existsByEmail("nobody@example.com")).isFalse();
    }

    @Test
    @Sql("/test-data/users.sql")   // load from SQL script before test:
    void findByRole_returnsAllAdmins() {
        List<User> admins = userRepository.findByRole(User.Role.ADMIN);
        assertThat(admins).hasSize(2);
    }
}

# ── test/resources/application.yml — test H2 configuration: ──────────
spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: false

# ── @Sql — run SQL scripts in a specific test: ────────────────────────
@Test
@Sql(scripts = "/test-data/users.sql",
     executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/test-data/cleanup.sql",
     executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void myTest() { ... }

# ── Separate H2 config for tests only: ───────────────────────────────
# src/test/resources/application.yml overrides main config for tests.
# @DataJpaTest uses an embedded database automatically —
# no datasource URL needed unless you want a specific H2 mode.

H2 vs Testcontainers — When to Use Each

H2 and Testcontainers serve different testing needs. Choosing correctly prevents false positives from H2 compatibility quirks and unnecessary slowness from Testcontainers in simple unit tests.
Shell
# ── Use H2 when: ─────────────────────────────────────────────────────
# - Testing repository logic that uses only standard SQL
# - Unit and slice tests (@DataJpaTest) where speed matters
# - JPQL queries, derived query methods, Spring Data features
# - CI pipelines where Docker is not available
# - Local development quick feedback loop

# Example — @DataJpaTest with H2 (fast, ~500ms startup):
@DataJpaTest
class UserRepositoryTest {
    @Autowired UserRepository userRepository;
    // Tests standard CRUD and JPQL — H2 is sufficient
}

# ── Use Testcontainers when: ──────────────────────────────────────────
# - Queries use database-specific syntax (ON CONFLICT, MERGE, JSON, arrays)
# - Native queries that H2 cannot parse
# - Testing Flyway migrations against the real database engine
# - Full-text search (PostgreSQL tsvector, MySQL FULLTEXT)
# - Stored procedures, functions, triggers
# - Verifying database constraints, indexes, and data types
# - Integration tests that must match production behaviour exactly

# Example — @SpringBootTest with Testcontainers (slower, ~10-30s startup):
@SpringBootTest
@Testcontainers
class ProductRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

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

    @Autowired ProductRepository productRepository;

    @Test
    void fullTextSearch_returnsMatchingProducts() {
        // Tests PostgreSQL-specific tsvector full-text search —
        // this would fail with H2
        List<Product> results = productRepository.fullTextSearch("widget");
        assertThat(results).isNotEmpty();
    }
}

# ── Recommended test strategy: ────────────────────────────────────────
# @DataJpaTest + H2        → repository unit tests (fast, many)
# @SpringBootTest + TC     → integration tests (slower, fewer, CI only)
# The two complement each other — neither replaces the other.