Spring Boot
Introduction to JPA
The Java Persistence API (JPA) is a specification for mapping Java objects to relational database tables — a technique known as Object-Relational Mapping (ORM). Spring Boot auto-configures Hibernate as the JPA provider, sets up a DataSource, manages the EntityManager lifecycle, and integrates Spring Data JPA repositories so you can perform database operations with minimal boilerplate.
What is JPA and How Spring Boot Uses It
JPA is a Java EE / Jakarta EE specification (javax.persistence / jakarta.persistence) that defines how Java objects — called entities — map to relational database rows. Hibernate is the default JPA provider Spring Boot auto-configures. Spring Data JPA adds a repository abstraction layer on top, eliminating manual DAO code.
Shell
# ── JPA stack in a Spring Boot application: ──────────────────────────
#
# Your Code → Spring Data JPA → JPA Specification → Hibernate → JDBC → Database
#
# Spring Boot auto-configures:
# • DataSource — connection pool (HikariCP by default)
# • EntityManagerFactory — JPA session factory backed by Hibernate
# • TransactionManager — JpaTransactionManager
# • Repositories — Spring Data JPA proxies for your interfaces
#
# ── Core JPA concepts: ───────────────────────────────────────────────
# Entity — a Java class mapped to a database table (@Entity)
# EntityManager — JPA API for CRUD and JPQL queries
# Repository — Spring Data interface (JpaRepository<T, ID>)
# Transaction — unit of work; managed by @Transactional
# JPQL — JPA Query Language; object-oriented, not table-oriented
# Criteria API — type-safe programmatic query buildingAdding JPA to a Spring Boot Project
Add the spring-boot-starter-data-jpa dependency and a JDBC driver for your database. Spring Boot detects the driver and auto-configures the DataSource, Hibernate dialect, and EntityManagerFactory.
XML
<!-- pom.xml — Spring Data JPA + H2 (in-memory, for development): -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Production — swap H2 for PostgreSQL: -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Production — or MySQL: -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>DataSource Configuration
Spring Boot reads DataSource settings from application.yml. For development, H2 runs in-memory with no setup. For production, supply the JDBC URL, credentials, and connection pool settings.
yaml
# ── application.yml — H2 in-memory (development): ────────────────────
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true # browser console at /h2-console
jpa:
hibernate:
ddl-auto: create-drop # create schema on startup, drop on shutdown
show-sql: true
properties:
hibernate:
format_sql: true
# ── application.yml — PostgreSQL (production): ────────────────────────
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: myuser
password: secret
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # validate schema matches entities; never alter
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialectDefining an Entity
An entity is a plain Java class annotated with @Entity. Each instance represents a row in the mapped table. JPA requires a no-arg constructor and a field annotated with @Id as the primary key.
Java
import jakarta.persistence.*;
@Entity
@Table(name = "products") // optional — defaults to class name
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment
private Long id;
@Column(name = "product_name", nullable = false, length = 200)
private String name;
@Column(nullable = false)
private BigDecimal price;
@Column(length = 2000)
private String description;
@Enumerated(EnumType.STRING) // store enum as a string, not ordinal
private ProductStatus status;
@CreationTimestamp // Hibernate extension
@Column(updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// ── JPA requires a public or protected no-arg constructor: ────────
protected Product() {}
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
this.status = ProductStatus.ACTIVE;
}
// getters and setters ...
}
public enum ProductStatus { ACTIVE, INACTIVE, DISCONTINUED }Spring Data JPA Repository
Extend JpaRepository<T, ID> to get a full suite of CRUD operations, pagination, and sorting with no implementation code. Spring Boot auto-detects repository interfaces and creates proxies at startup.
Java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
// ── Derived query methods — Spring generates SQL from the method name: ──
List<Product> findByStatus(ProductStatus status);
Optional<Product> findByName(String name);
List<Product> findByPriceLessThan(BigDecimal maxPrice);
List<Product> findByStatusAndPriceBetween(
ProductStatus status, BigDecimal min, BigDecimal max);
boolean existsByName(String name);
long countByStatus(ProductStatus status);
// ── Custom JPQL query (object-oriented, uses class and field names): ──
@Query("SELECT p FROM Product p WHERE p.price > :minPrice ORDER BY p.price DESC")
List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
// ── Native SQL query: ────────────────────────────────────────────────
@Query(value = "SELECT * FROM products WHERE product_name ILIKE %:keyword%",
nativeQuery = true)
List<Product> searchByKeyword(@Param("keyword") String keyword);
}
// ── Built-in JpaRepository methods (no code needed): ─────────────────
// save(entity) — INSERT or UPDATE
// findById(id) — SELECT by PK → Optional<T>
// findAll() — SELECT all
// findAll(Pageable) — SELECT with pagination
// deleteById(id) — DELETE by PK
// existsById(id) — SELECT count > 0
// count() — SELECT count(*)Using the Repository in a Service
Inject the repository into a @Service class. Annotate write operations with @Transactional so Hibernate flushes changes to the database automatically. Read-only methods benefit from @Transactional(readOnly = true) for performance optimisations.
Java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
@Transactional(readOnly = true) // default for all methods in this class
public class ProductService {
private final ProductRepository productRepository;
// Constructor injection — preferred over @Autowired on field:
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> findAll() {
return productRepository.findAll();
}
public Product findById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(
"Product not found: " + id));
}
@Transactional // overrides class-level readOnly = true
public Product create(Product product) {
if (productRepository.existsByName(product.getName())) {
throw new IllegalArgumentException(
"Product already exists: " + product.getName());
}
return productRepository.save(product); // INSERT
}
@Transactional
public Product updatePrice(Long id, BigDecimal newPrice) {
Product product = findById(id);
product.setPrice(newPrice);
return productRepository.save(product); // UPDATE
// Hibernate detects the dirty field and generates UPDATE SQL.
}
@Transactional
public void delete(Long id) {
productRepository.deleteById(id); // DELETE
}
}Schema Generation with ddl-auto
Hibernate's ddl-auto property controls whether it creates, updates, or validates the database schema on startup. Use create-drop for tests, validate or none in production — and manage production schemas with Flyway or Liquibase.
yaml
# ── spring.jpa.hibernate.ddl-auto values: ────────────────────────────
#
# none — do nothing (safest for production)
# validate — validate schema matches entities; throw if mismatch
# update — alter existing tables to match entities (risky in prod)
# create — drop and recreate schema on startup
# create-drop — create on startup, drop on shutdown (ideal for tests)
#
# Recommended per environment:
# Development / tests:
spring:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
# Staging (validate against migration-managed schema):
spring:
jpa:
hibernate:
ddl-auto: validate
# Production (Flyway or Liquibase owns the schema):
spring:
jpa:
hibernate:
ddl-auto: none
# ── Tip — use Flyway for production migrations: ───────────────────────
# Add spring-boot-starter-flyway; place SQL in src/main/resources/db/migration/
# V1__create_products_table.sql
# V2__add_status_column.sql
# Flyway applies pending migrations on startup, tracks applied versions.