Home / Spring Boot / Data and Transactions / Spring Data JPA

Spring Data JPA

Spring Data JPA removes almost all database boilerplate. You write an interface, and Spring creates the implementation with ready-made CRUD, paging, sorting and query methods on top of JPA and Hibernate.

Setup

spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.datasource.username=app
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
ddl-autoEffectUse
noneDoes nothingProduction with migrations
validateChecks that tables match entitiesProduction (recommended with Flyway)
updateAdds missing tables and columnsQuick prototypes only
createDrops and creates schema at startTests
create-dropCreates at start, drops at shutdownIn-memory test databases

Entity

import jakarta.persistence.*;
import java.math.BigDecimal;

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

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

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    private String category;

    protected Product() { }                       // required by JPA

    public Product(String name, BigDecimal price, String category) {
        this.name = name; this.price = price; this.category = category;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public String getCategory() { return category; }
}

Repository

Repository  ◄─ CrudRepository  ◄─ ListCrudRepository  ◄─┐
                       PagingAndSortingRepository  ◄──────────┼─ JpaRepository
save, findById, findAll, deleteById, count, existsById, findAll(Pageable) ...
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {

    List<Product> findByCategory(String category);
    Optional<Product> findByNameIgnoreCase(String name);
    List<Product> findByPriceGreaterThan(BigDecimal price);
    List<Product> findByNameContainingIgnoreCase(String text);
    List<Product> findTop3ByOrderByPriceDesc();
    boolean existsByName(String name);
    long countByCategory(String category);
    Page<Product> findByCategory(String category, Pageable pageable);

    @Query("select p from Product p where p.price between :min and :max order by p.price")
    List<Product> findInRange(@Param("min") BigDecimal min, @Param("max") BigDecimal max);

    @Query(value = "select * from products where category = ?1 limit 5", nativeQuery = true)
    List<Product> firstFiveNative(String category);

    @Modifying
    @Transactional
    @Query("update Product p set p.price = p.price * :factor where p.category = :category")
    int raisePrices(@Param("category") String category, @Param("factor") BigDecimal factor);
}
Method name partMeaning
findBy / readBy / getBySelect
countBy / existsBy / deleteByCount, check, delete
And, OrCombine conditions
GreaterThan, LessThan, BetweenComparisons
Like, Containing, StartingWith, IgnoreCaseText search
OrderByPriceDescSorting
Top3, FirstLimit results

Paging and sorting

Pageable pageable = PageRequest.of(0, 10, Sort.by("price").descending());
Page<Product> page = repository.findByCategory("books", pageable);
System.out.println(page.getContent());
System.out.println(page.getTotalElements() + " total, " + page.getTotalPages() + " pages");

Projections: read only what you need

public interface ProductSummary {       // interface projection
    String getName();
    BigDecimal getPrice();
}
List<ProductSummary> findByCategory(String category);     // in the repository

public record ProductView(String name, BigDecimal price) { }   // class-based (DTO) projection
List<ProductView> findViewByCategory(String category);

How an interface becomes a working repository (internal workflow)

@EnableJpaRepositories (auto-enabled by Boot)
   └─ JpaRepositoriesRegistrar scans for interfaces extending Repository
        for each ProductRepository: register a bean definition of type JpaRepositoryFactoryBean
   ▼ at bean creation:
   JpaRepositoryFactory.getRepository(ProductRepository.class)
      1. target object     = SimpleJpaRepository<Product, Long>   (implements save, findById, findAll, delete ... with EntityManager)
      2. custom fragments  = your *RepositoryImpl classes (if any)
      3. proxy             = ProxyFactory (JDK proxy of the interface) with interceptors:
            ├─ TransactionalRepositoryProxyPostProcessor   @Transactional(readOnly=true) on reads
            ├─ PersistenceExceptionTranslationInterceptor  JPA exceptions -> DataAccessException
            ├─ QueryExecutorMethodInterceptor              for query methods (findByName, @Query)
            └─ ImplementationMethodExecutionInterceptor    delegates CRUD calls to SimpleJpaRepository
   ▼ query method lookup at startup (QueryLookupStrategy CREATE_IF_NOT_FOUND):
      @Query present?   ─► use it      named query (Product.findByName) exists? ─► use it
      otherwise: parse the method name with PartTree:  findBy Name And PriceGreaterThan ─► subject / predicate parts ─► JPA Criteria query
      a method name that cannot be parsed ─► the application FAILS AT STARTUP ("No property xyz found for type Product")

Because query methods are validated at startup, typos in property names fail fast. @Query strings are checked when the application starts too (JPQL syntax is validated by Hibernate).

Custom repository behavior (fragments)

public interface ProductRepositoryCustom {
    List<Product> searchByTerms(List<String> terms);
}

// The name must be <FragmentInterface>Impl
public class ProductRepositoryCustomImpl implements ProductRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    @Override
    public List<Product> searchByTerms(List<String> terms) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Product> query = cb.createQuery(Product.class);
        Root<Product> root = query.from(Product.class);
        Predicate[] predicates = terms.stream()
                .map(t -> cb.like(cb.lower(root.get("name")), "%" + t.toLowerCase() + "%"))
                .toArray(Predicate[]::new);
        query.select(root).where(cb.or(predicates));
        return em.createQuery(query).getResultList();
    }
}

public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom { }
// productRepository.searchByTerms(List.of("pen", "book"))  now works next to all generated methods

Stream large results without loading everything

public interface ProductRepository extends JpaRepository<Product, Long> {
    @QueryHints(@QueryHint(name = HINT_FETCH_SIZE, value = "500"))
    @Query("select p from Product p")
    Stream<Product> streamAll();
}

@Transactional(readOnly = true)                                   // the stream needs an open transaction
public void export(Writer out) {
    try (Stream<Product> products = repository.streamAll()) {     // ALWAYS close: try-with-resources
        products.forEach(p -> write(out, p));
    }
}

Query methods by name: the grammar

KeywordJPQL resultExample
And / Orand / orfindByNameAndCategory
Betweenbetween ?1 and ?2findByPriceBetween(min, max)
LessThan, GreaterThanEqual< , >=findByStockLessThan(5)
IsNull / IsNotNullis nullfindByDeletedAtIsNull()
In / NotInin (?1)findByCategoryIn(List<String>)
Like / Containing / StartingWith / EndingWithlikefindByNameStartingWithIgnoreCase
True / False= truefindByActiveTrue()
OrderBy...Asc/Descorder byfindByCategoryOrderByPriceDesc
Top / First / Distinctlimit / distinctfindTop5ByCategoryOrderByPriceAsc
Nested propertyjoin on a relationfindByCustomerAddressCity(String city)

Typical exceptions

ExceptionMeaning
DataIntegrityViolationExceptionConstraint failed: unique key, not null, foreign key. Map it to 409 or 400
EmptyResultDataAccessExceptiondeleteById or single result method found nothing
IncorrectResultSizeDataAccessExceptionA method returning one item found several
ObjectOptimisticLockingFailureExceptionVersion conflict with @Version
InvalidDataAccessApiUsageExceptionWrong API usage, for example a modifying query without @Modifying or a transaction
PropertyReferenceExceptionQuery method name refers to a property that does not exist (startup failure)
@DataJpaTest                                                // slice: only JPA, embedded database, rolls back after each test
class ProductRepositoryTest {
    @Autowired ProductRepository repository;

    @Test
    void findsTopThreeByPrice() {
        repository.saveAll(List.of(new Product("A", new BigDecimal("10"), "x"), new Product("B", new BigDecimal("30"), "x"),
                                   new Product("C", new BigDecimal("20"), "x"), new Product("D", new BigDecimal("5"), "x")));
        assertThat(repository.findTop3ByOrderByPriceDesc()).extracting(Product::getName).containsExactly("B", "C", "A");
    }
}

Common mistake: Using spring.jpa.hibernate.ddl-auto=update in production. It can change your schema unexpectedly. Use validate together with migrations such as Flyway.

Interview questions

What is the difference between CrudRepository and JpaRepository?

JpaRepository extends the CRUD and paging repositories and adds JPA-specific methods such as flush() and saveAllAndFlush().

When does save() insert and when does it update?

If the entity is new (id is null) it inserts. If it has an id that exists, it merges and updates.

Derived query or @Query?

Use derived methods for simple queries. Use @Query when the method name would be too long or the query is complex.

Why does my application fail at startup with 'No property xyz found'?

A derived query method uses a name that does not match an entity property. Spring Data parses method names at startup and fails fast.

Do repository methods run inside a transaction?

Yes. Read methods use readOnly transactions and write methods use normal ones (from SimpleJpaRepository). Your service @Transactional usually wraps them into one bigger transaction.