Skip to content
Spring Boot core 3 min read

Spring Data JPA

Spring Data JPA eliminates most of the boilerplate around relational persistence. You define entities and repository interfaces, and Spring generates the implementation at runtime, backed by Hibernate as the default JPA provider.

Defining an entity

An @Entity maps a Java class to a database table. JPA needs an identifier and a no-args constructor.

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private BigDecimal price;

    protected Product() { } // required by JPA

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }
    // getters and setters
}

Note: GenerationType.IDENTITY delegates ID generation to the database’s auto-increment column, the right choice for most MySQL and PostgreSQL schemas.

The repository interface

Extend JpaRepository<T, ID> and you immediately get CRUD methods, pagination, and sorting, no implementation required.

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Out of the box you can call save, findById, findAll, deleteById, count, and findAll(Pageable).

Derived query methods

Spring parses method names and builds the query for you. The naming convention is findBy + property + optional keyword.

public interface ProductRepository extends JpaRepository<Product, Long> {

    List<Product> findByName(String name);

    List<Product> findByPriceLessThan(BigDecimal max);

    List<Product> findByNameContainingIgnoreCase(String fragment);

    Optional<Product> findFirstByOrderByPriceDesc();
}
KeywordGenerated condition
findByNameAndPricename = ? AND price = ?
findByPriceBetweenprice BETWEEN ? AND ?
findByNameStartingWithname LIKE ?%
findByActiveTrueactive = true

Custom queries with @Query

When method names get unwieldy, write JPQL (or native SQL) explicitly.

@Query("SELECT p FROM Product p WHERE p.price > :floor ORDER BY p.price")
List<Product> pricierThan(@Param("floor") BigDecimal floor);

@Query(value = "SELECT * FROM products WHERE price > ?1", nativeQuery = true)
List<Product> pricierThanNative(BigDecimal floor);

Relationships overview

JPA models associations with mapping annotations. Always specify the fetch strategy deliberately.

AnnotationRelationshipDefault fetch
@OneToManyParent → childrenLAZY
@ManyToOneChild → parentEAGER
@OneToOneOne-to-oneEAGER
@ManyToManyMany-to-manyLAZY
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<LineItem> items = new ArrayList<>();
}

Warning: Default @ManyToOne and @OneToOne fetching is EAGER, which can trigger surprise joins and the N+1 query problem. Set fetch = FetchType.LAZY and load what you need explicitly.

A working example with H2

H2 is an in-memory database, ideal for demos and tests. Add the dependency and configure it.

spring:
  datasource:
    url: jdbc:h2:mem:shopdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  h2:
    console:
      enabled: true   # browse at /h2-console

Seed and query the repository at startup with a CommandLineRunner.

@Bean
CommandLineRunner seed(ProductRepository repo) {
    return args -> {
        repo.save(new Product("Keyboard", new BigDecimal("79.99")));
        repo.save(new Product("Mouse", new BigDecimal("39.50")));
        repo.findByPriceLessThan(new BigDecimal("50"))
            .forEach(p -> System.out.println("Cheap: " + p.getName()));
    };
}

Output:

Hibernate: insert into products (name, price) values (?, ?)
Hibernate: insert into products (name, price) values (?, ?)
Hibernate: select ... from products p where p.price < ?
Cheap: Mouse

Best Practices

  • Set fetch types explicitly; prefer LAZY for associations and fetch eagerly only when needed via JOIN FETCH.
  • Use ddl-auto: validate (or Flyway/Liquibase migrations) in production; never update or create against real data.
  • Keep transactional logic in @Service methods annotated with @Transactional, not in repositories.
  • Return Optional from single-result finders to make absence explicit.
  • Map entities to DTOs before returning them from controllers to avoid lazy-loading serialization issues.
  • Watch the generated SQL with show-sql during development and eliminate N+1 queries.
Last updated June 1, 2026
Was this helpful?