By WebNest Studio Engineering Team
Spring Boot Tutorial
Spring Data JPA
Spring Data JPA removes the boilerplate of writing DAO classes by hand. You define a repository interface, and Spring generates the implementation at runtime — including full CRUD operations and, for many common query shapes, even the query itself, derived directly from the method name.
JpaRepository and Built-In CRUD
Extending JpaRepository<EntityType, IdType> immediately gives you methods like save(), findById(), findAll(), deleteById(), and count() — with no implementation code required. Spring Data generates a proxy implementation at startup and registers it as a bean you can simply @Autowire or inject via constructor.
Derived Query Methods
Spring Data JPA can parse a method name and build the corresponding query automatically, as long as it follows the expected keyword conventions — findBy, And, Or, OrderBy, GreaterThan, Containing, and more. For example, findByEmailAndActiveTrue(String email) generates a query filtering on both the email column and an active flag, with zero SQL or JPQL written by hand.
Custom Queries with @Query
When a query is too complex to express as a method name, or you need specific join/aggregation logic, @Query lets you write JPQL (object-oriented, referencing entity fields) or native SQL (with nativeQuery = true) directly above the method signature, while Spring Data still handles binding the method's parameters into the query.
Return Types: Optional, Counts and Existence Checks
The return type of a repository method is part of the query. A method returning Optional<Product> gives you an empty Optional instead of null when nothing matches, which forces the caller to decide what "not found" means. countBy… returns a number computed in the database, and existsBy… returns a boolean without loading any entity, which is far cheaper than fetching a list just to check whether it is empty. findFirstBy… or findTop3By… limit the result size in SQL.
Bulk Updates with @Modifying
A JPQL UPDATE or DELETE needs @Modifying next to @Query, and it must run inside a transaction, so annotate the method or its caller with @Transactional. The method returns how many rows changed. Bulk statements go straight to the database and skip the persistence context: entities you already loaded in the same transaction keep their old values unless you set @Modifying(clearAutomatically = true) or reload them.
Choosing How to Write a Query
- Derived method names for simple filters on one or two fields. They are checked at startup, so a typo fails fast.
- @Query with JPQL when the name would get long, or you need joins, grouping or a bulk update.
- Native SQL (
nativeQuery = true) only for database-specific features such as window functions or full-text search, because it ties the query to one database. - Specifications (
JpaSpecificationExecutor) when users can combine many optional filters, such as a product search page, where writing one method per combination is impossible.
When Spring Data JPA Is the Right Tool
It fits applications that mostly load, change and save business objects: orders, users, products. It is a weaker fit for reporting queries over millions of rows or for heavy batch jobs, where loading every row as a managed entity wastes memory. There, a projection, JdbcClient or a dedicated batch tool is usually faster, and you can use both in the same application.
Examples
A repository combining CRUD, derived queries, and a custom @Query
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived query: SELECT * FROM product WHERE category = ? AND price < ?
List<Product> findByCategoryAndPriceLessThan(String category, BigDecimal price);
// Derived query with ordering
List<Product> findByCategoryOrderByPriceAsc(String category);
// Custom JPQL query for a case a method name can't express cleanly
@Query("SELECT p FROM Product p WHERE p.stock < :threshold")
List<Product> findLowStockProducts(@Param("threshold") int threshold);
}
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) { this.repository = repository; }
public List<Product> cheapElectronics() {
return repository.findByCategoryAndPriceLessThan("Electronics", new BigDecimal("50.00"));
}
}
cheapElectronics() ->
[Product{id=3, name='USB Cable', price=8.99}, Product{id=7, name='Phone Stand', price=14.50}]
A complete, runnable catalogue: entity, repository and a startup runner (tested on Spring Boot 4.1.1 with H2)
// Product.java
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String category;
private BigDecimal price;
private int stock;
protected Product() { } // JPA needs a no-argument constructor
public Product(String name, String category, String price, int stock) {
this.name = name;
this.category = category;
this.price = new BigDecimal(price);
this.stock = stock;
}
@Override
public String toString() { return name + " (" + price + ", stock " + stock + ")"; }
}
// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategoryAndPriceLessThan(String category, BigDecimal price);
List<Product> findByCategoryOrderByPriceAsc(String category);
Optional<Product> findFirstByNameIgnoreCase(String name);
long countByStockLessThan(int stock);
boolean existsByName(String name);
@Query("SELECT p FROM Product p WHERE p.stock < :threshold ORDER BY p.stock")
List<Product> findLowStock(@Param("threshold") int threshold);
@Transactional
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock + :amount WHERE p.category = :category")
int restock(@Param("category") String category, @Param("amount") int amount);
}
// CatalogDemo.java
@Component
public class CatalogDemo implements CommandLineRunner {
private final ProductRepository products;
public CatalogDemo(ProductRepository products) {
this.products = products;
}
@Override
public void run(String... args) {
products.saveAll(List.of(
new Product("USB Cable", "Electronics", "8.99", 120),
new Product("Phone Stand", "Electronics", "14.50", 4),
new Product("Mechanical Keyboard", "Electronics", "89.00", 2),
new Product("Notebook", "Stationery", "3.25", 300)));
System.out.println("count = " + products.count());
System.out.println("cheap electronics = "
+ products.findByCategoryAndPriceLessThan("Electronics", new BigDecimal("50.00")));
System.out.println("electronics by price = " + products.findByCategoryOrderByPriceAsc("Electronics"));
System.out.println("find 'notebook' = " + products.findFirstByNameIgnoreCase("notebook").orElse(null));
System.out.println("find 'stapler' = " + products.findFirstByNameIgnoreCase("stapler").orElse(null));
System.out.println("low stock (< 5) = " + products.findLowStock(5));
System.out.println("rows restocked = " + products.restock("Electronics", 10));
System.out.println("low stock after restock = " + products.countByStockLessThan(5));
System.out.println("exists 'USB Cable' = " + products.existsByName("USB Cable"));
}
}
// pom.xml: spring-boot-starter-data-jpa and com.h2database:h2 (runtime scope)
count = 4
cheap electronics = [USB Cable (8.99, stock 120), Phone Stand (14.50, stock 4)]
electronics by price = [USB Cable (8.99, stock 120), Phone Stand (14.50, stock 4), Mechanical Keyboard (89.00, stock 2)]
find 'notebook' = Notebook (3.25, stock 300)
find 'stapler' = null
low stock (< 5) = [Mechanical Keyboard (89.00, stock 2), Phone Stand (14.50, stock 4)]
rows restocked = 3
low stock after restock = 0
exists 'USB Cable' = true
Common Mistakes
- Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it.
- Expecting entities loaded earlier in the same transaction to see the result of a bulk @Modifying query. Bulk statements bypass the persistence context; use clearAutomatically = true or reload the entity.
- Loading a whole list with findBy… just to check whether it is empty, instead of using existsBy… or countBy…, which the database answers without building entities.
- Misspelling an entity field name inside a derived query method name — Spring Data fails at application startup with a clear but often overlooked "No property X found" error.
- Writing overly long derived method names for complex filters instead of switching to @Query once readability suffers.
- Forgetting @Param in a @Query method when using named parameters (:name) instead of positional ones.
- Calling findById() and directly using .get() on the returned Optional without checking presence, risking a NoSuchElementException.
Exercise
A teammate is about to make this mistake: Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it. Use JpaRepository and Built-In CRUD to review the decision. Write a corrected implementation or design sketch that follows JpaRepository provides full CRUD operations with zero implementation code., then name one edge case you would check.
Show hints
- Start with JpaRepository and Built-In CRUD and compare your approach with A repository combining CRUD, derived queries, and a custom @Query. Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows JpaRepository provides full CRUD operations with zero implementation code. and that it would catch the failure described by Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it..
Quick quiz
1. Which choice best describes the recommended approach in Spring Data JPA?
- A. Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it.
- B. Expecting entities loaded earlier in the same transaction to see the result of a bulk @Modifying query. Bulk statements bypass the persistence context; use clearAutomatically = true or reload the entity.
- C. JpaRepository provides full CRUD operations with zero implementation code.
Show answer
C. This is the recommended approach: JpaRepository provides full CRUD operations with zero implementation code. Avoid: Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it.
Interview questions
Why does this principle matter in Spring Data JPA: JpaRepository provides full CRUD operations with zero implementation code.
Show answer
JpaRepository provides full CRUD operations with zero implementation code. Apply this in JpaRepository and Built-In CRUD; the example A repository combining CRUD, derived queries, and a custom @Query shows one way to do it. Avoid Writing an @Modifying UPDATE or DELETE without a transaction, which fails with InvalidDataAccessApiUsageException caused by TransactionRequiredException ("No active transaction"). Put @Transactional on the repository method or on the service method that calls it. and explain how your implementation or design prevents that failure.
Key Points to Remember
- JpaRepository provides full CRUD operations with zero implementation code.
- Derived query methods build queries automatically from a method name following Spring Data's keyword conventions.
- @Query supports JPQL or native SQL for queries too complex to express as a method name.
- Repository interfaces are auto-implemented as proxy beans at application startup.
- Return types shape the query: Optional for one result, countBy/existsBy for cheap checks, findFirst/findTopN to limit rows.
- Bulk UPDATE/DELETE queries need @Modifying and a transaction, and they skip the persistence context.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering