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-auto | Effect | Use |
|---|---|---|
| none | Does nothing | Production with migrations |
| validate | Checks that tables match entities | Production (recommended with Flyway) |
| update | Adds missing tables and columns | Quick prototypes only |
| create | Drops and creates schema at start | Tests |
| create-drop | Creates at start, drops at shutdown | In-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 part | Meaning |
|---|---|
| findBy / readBy / getBy | Select |
| countBy / existsBy / deleteBy | Count, check, delete |
| And, Or | Combine conditions |
| GreaterThan, LessThan, Between | Comparisons |
| Like, Containing, StartingWith, IgnoreCase | Text search |
| OrderByPriceDesc | Sorting |
| Top3, First | Limit 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 methodsStream 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
| Keyword | JPQL result | Example |
|---|---|---|
| And / Or | and / or | findByNameAndCategory |
| Between | between ?1 and ?2 | findByPriceBetween(min, max) |
| LessThan, GreaterThanEqual | < , >= | findByStockLessThan(5) |
| IsNull / IsNotNull | is null | findByDeletedAtIsNull() |
| In / NotIn | in (?1) | findByCategoryIn(List<String>) |
| Like / Containing / StartingWith / EndingWith | like | findByNameStartingWithIgnoreCase |
| True / False | = true | findByActiveTrue() |
| OrderBy...Asc/Desc | order by | findByCategoryOrderByPriceDesc |
| Top / First / Distinct | limit / distinct | findTop5ByCategoryOrderByPriceAsc |
| Nested property | join on a relation | findByCustomerAddressCity(String city) |
Typical exceptions
| Exception | Meaning |
|---|---|
| DataIntegrityViolationException | Constraint failed: unique key, not null, foreign key. Map it to 409 or 400 |
| EmptyResultDataAccessException | deleteById or single result method found nothing |
| IncorrectResultSizeDataAccessException | A method returning one item found several |
| ObjectOptimisticLockingFailureException | Version conflict with @Version |
| InvalidDataAccessApiUsageException | Wrong API usage, for example a modifying query without @Modifying or a transaction |
| PropertyReferenceException | Query 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.