Setting Up Spring Boot with Spring Data JPA and MySQL
Introduction
This article builds the project foundation used throughout the entire series. By the end, you will have a running Spring Boot 3.3 application connected to MySQL 8.x with Hibernate 6, a proper connection pool, schema management via Flyway, and SQL logging configured so you can see exactly what Hibernate sends to the database.
Project Setup
Maven pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<groupId>com.devopsmonk</groupId>
<artifactId>spring-data-jpa-demo</artifactId>
<version>1.0.0</version>
<name>spring-data-jpa-demo</name>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Data JPA — includes Hibernate 6 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Web (for REST controllers in later articles) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL JDBC driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Flyway for schema migrations -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<!-- Lombok — reduces boilerplate on entities and services -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
The key dependency is spring-boot-starter-data-jpa, which pulls in:
- Spring Data JPA
- Hibernate 6.4+
- Spring ORM
- The JPA API (
jakarta.persistence)
Main Application Class
package com.devopsmonk.jpademo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JpaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(JpaDemoApplication.class, args);
}
}
No extra annotations are needed. Spring Boot auto-configures JPA, Hibernate, and the datasource from application.properties.
Application Configuration
# ── DataSource ───────────────────────────────────────────────────────────────
spring.datasource.url=jdbc:mysql://localhost:3306/jpa_demo?createDatabaseIfNotExist=true\
&useSSL=false\
&serverTimezone=UTC\
&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ── HikariCP Connection Pool ─────────────────────────────────────────────────
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
# ── JPA / Hibernate ──────────────────────────────────────────────────────────
# Never let Hibernate manage schema in production — use Flyway instead
spring.jpa.hibernate.ddl-auto=validate
# Show SQL in logs (disable in production)
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Hibernate statistics — useful during development for detecting N+1
spring.jpa.properties.hibernate.generate_statistics=false
# ── Flyway ───────────────────────────────────────────────────────────────────
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
Why ddl-auto=validate?
Hibernate’s DDL auto options:
| Value | Behaviour | Use when |
|---|---|---|
none | No schema changes | Production with managed migrations |
validate | Validates schema matches entities — fails fast on mismatch | Production (recommended) |
update | Adds missing columns — never drops | Prototyping only — dangerous in prod |
create | Drops and recreates all tables on startup | Tests with in-memory DB |
create-drop | Creates on start, drops on stop | Tests only |
In production, use validate with Flyway managing the schema. In development, update is convenient but can hide errors. This series uses validate so you see Flyway doing its job.
Flyway Migration Scripts
Flyway applies SQL migrations from src/main/resources/db/migration/ in alphabetical order.
V1__create_schema.sql
-- Initial e-commerce schema
CREATE TABLE categories (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE tags (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
category_id BIGINT,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES categories (id)
) ENGINE=InnoDB;
CREATE TABLE product_tags (
product_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY (product_id, tag_id),
CONSTRAINT fk_product_tag_product FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT fk_product_tag_tag FOREIGN KEY (tag_id) REFERENCES tags (id)
) ENGINE=InnoDB;
CREATE TABLE customers (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
phone VARCHAR(20),
-- Address embedded columns
street VARCHAR(200),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(50),
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
ordered_at DATETIME(6) NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customers (id)
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT NOT NULL AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_item_product FOREIGN KEY (product_id) REFERENCES products (id)
) ENGINE=InnoDB;
CREATE TABLE reviews (
id BIGINT NOT NULL AUTO_INCREMENT,
product_id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
rating TINYINT NOT NULL,
comment TEXT,
created_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_review_product FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT fk_review_customer FOREIGN KEY (customer_id) REFERENCES customers (id)
) ENGINE=InnoDB;
Each subsequent migration (V2, V3, …) adds columns, tables, or indexes as the series progresses.
SQL Logging in Development
Seeing Hibernate’s generated SQL is essential for learning and debugging. The show-sql setting logs raw SQL; format_sql=true pretty-prints it.
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Output looks like:
Hibernate:
select
c1_0.id,
c1_0.name,
c1_0.email
from
customers c1_0
where
c1_0.email=?
For even more detail (including bind parameters), add:
logging.level.org.hibernate.orm.jdbc.bind=TRACE
This logs the actual values substituted for ? — invaluable during debugging:
binding parameter (1:VARCHAR) <- [alice@example.com]
Turn both off in production — SQL logging is a significant overhead in high-throughput applications.
Verifying the Setup
Create a minimal entity and repository to confirm everything works:
// src/main/java/com/devopsmonk/jpademo/domain/Category.java
package com.devopsmonk.jpademo.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "categories")
@Getter @Setter @NoArgsConstructor
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String slug;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// src/main/java/com/devopsmonk/jpademo/repository/CategoryRepository.java
package com.devopsmonk.jpademo.repository;
import com.devopsmonk.jpademo.domain.Category;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
// src/main/java/com/devopsmonk/jpademo/DemoRunner.java
package com.devopsmonk.jpademo;
import com.devopsmonk.jpademo.domain.Category;
import com.devopsmonk.jpademo.repository.CategoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@RequiredArgsConstructor
@Slf4j
public class DemoRunner implements CommandLineRunner {
private final CategoryRepository categoryRepository;
@Override
public void run(String... args) {
Category category = new Category();
category.setName("Electronics");
category.setSlug("electronics");
category.setCreatedAt(LocalDateTime.now());
category.setUpdatedAt(LocalDateTime.now());
Category saved = categoryRepository.save(category);
log.info("Saved category with id={}", saved.getId());
long count = categoryRepository.count();
log.info("Total categories in database: {}", count);
}
}
Run the application and you will see:
Hibernate:
insert
into
categories
(created_at, name, slug, updated_at)
values
(?, ?, ?, ?)
INFO DemoRunner - Saved category with id=1
INFO DemoRunner - Total categories in database: 1
Flyway creates the schema, Hibernate validates it matches the entity, and the repository saves the record — everything is wired up.
Docker Compose for Local MySQL
If you do not have MySQL installed locally, use Docker Compose:
# docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: jpa_demo
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Start with docker-compose up -d before running the application.
Key Takeaways
spring-boot-starter-data-jpapulls in everything: Spring Data JPA, Hibernate 6, the JPA API, and Spring ORM- Never use
ddl-auto=updateorddl-auto=createin production — usevalidatewith Flyway for schema management - HikariCP is the default connection pool; configure
maximum-pool-sizeto match your thread model show-sql=trueandformat_sql=truemake Hibernate’s SQL visible during development — essential for learning and debugging- The Flyway
db/migrationdirectory is where all schema changes live — version-controlled SQL scripts
What’s Next
Article 3 explains the persistence context — the first-level cache that is the foundation of all JPA behaviour. Understanding it is required to understand everything else: dirty checking, lazy loading, transaction scope, and detached entity issues.