Spring BootIntroduction to JPA
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 building

Adding 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.PostgreSQLDialect

Defining 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.