Spring Data JPA: Complete Guide — Repositories, Queries, and Projections

Spring Data JPA eliminates the EntityManager boilerplate that used to fill every data-access class — you describe the query in a method name or a @Query annotation, Spring generates the implementation at startup. In practice it is both simpler than writing raw JDBC and more dangerous than it looks, because the abstractions hide decisions that have real performance consequences. I have spent four years working with Spring Data JPA across teams of varying sizes — from small startups running H2 in development to larger orgs running Hibernate 6.4 against PostgreSQL under sustained load. The N+1 queries, the LazyInitializationException stack traces, the open-session-in-view debates — I have lived all of them. This guide covers every technique you will encounter in a production Spring Boot application, with honest notes on where each approach breaks down. Tested on Spring Boot 3.4.1, Hibernate 6.4, Spring Data JPA 3.4, Java 21, PostgreSQL 16.


The Three Spring Data JPA Mistakes I See in Almost Every Codebase

Before the setup code: these are the problems that appear in code reviews and postmortems more than any others. Knowing them upfront changes how you read the rest of this guide.

1. spring.jpa.open-in-view=true left on in production. This is the Spring Boot default and it is the wrong default for production. It keeps the Hibernate session open for the entire HTTP request — including the serialisation phase — which means lazy collections can be fetched during JSON serialisation, silently. The result is N+1 queries that only appear at runtime under load, not in tests. Set spring.jpa.open-in-view=false in every production application.properties and use explicit @Transactional boundaries or fetch joins instead.

2. findAll() called on collections with no pagination. employeeRepository.findAll() returns every row in the table. In development with 50 rows, this is fine. In production with 500,000 rows, it loads half a gigabyte into the JVM heap. Use findAll(Pageable) everywhere a list endpoint could grow unbounded.

3. @OneToMany with FetchType.EAGER. EAGER on a collection means every findById() on the parent entity also fetches the entire child collection — even when you only needed the parent’s name. The default for @OneToMany is LAZY; do not override it without a specific, justified reason.

Project Setup

Add the Spring Data JPA starter and a database driver to your pom.xml. The starter pulls in Hibernate as the JPA implementation and configures everything automatically when Spring Boot detects a DataSource on the classpath.

<dependencies>
    <!-- Spring Data JPA: brings in Hibernate, Spring TX, and JPA API -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 for development / testing; swap for MySQL, PostgreSQL, etc. in production -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Test slice support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
# application.properties — minimal configuration for development
spring.datasource.url=jdbc:h2:mem:demodb   # in-memory H2 database
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop  # recreate schema on every start (dev only)
spring.jpa.show-sql=true                   # log SQL to console
spring.jpa.properties.hibernate.format_sql=true  # pretty-print the SQL

2. The Entity

Spring Data JPA works with standard JPA entities. The entity class remains a plain Java class — never a record, because JPA requires a no-argument constructor and mutable state.

package com.example.entity;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity                            // marks this class as a JPA entity
@Table(name = "employees")        // maps to the 'employees' table (optional if names match)
public class Employee {

    @Id                                                 // primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment in the DB
    private Long id;

    @Column(nullable = false, length = 100)             // NOT NULL, max 100 chars
    private String fullName;

    @Column(nullable = false, length = 50)
    private String department;

    @Column(nullable = false)
    private LocalDate hireDate;

    @Column(nullable = false)
    private double salary;

    // JPA requires a no-argument constructor (can be protected to discourage direct use)
    protected Employee() { }

    // Convenience constructor for creating new employees (id is assigned by the database)
    public Employee(String fullName, String department, LocalDate hireDate, double salary) {
        this.fullName   = fullName;
        this.department = department;
        this.hireDate   = hireDate;
        this.salary     = salary;
    }

    // Getters — Spring Data and Jackson use these
    public Long      getId()         { return id; }
    public String    getFullName()   { return fullName; }
    public String    getDepartment() { return department; }
    public LocalDate getHireDate()   { return hireDate; }
    public double    getSalary()     { return salary; }

    // Setters — needed for JPA to hydrate entities from the database
    public void setFullName(String fullName)     { this.fullName   = fullName; }
    public void setDepartment(String department) { this.department = department; }
    public void setHireDate(LocalDate hireDate)  { this.hireDate   = hireDate; }
    public void setSalary(double salary)         { this.salary     = salary; }
}

3. The Repository Hierarchy

Spring Data JPA provides a hierarchy of repository interfaces. Each level adds more operations. Choose the lowest level that gives you what you need to keep the API surface small.

InterfaceOperations addedWhen to use
Repository<T, ID>Marker only — no methodsDefine a completely custom API
CrudRepository<T, ID>save, findById, findAll, delete, countSimple CRUD, no pagination
PagingAndSortingRepository<T, ID>Adds findAll(Pageable) and findAll(Sort)When you need pagination/sorting
JpaRepository<T, ID>Adds flush, saveAndFlush, deleteAllInBatch, getReferenceByIdMost production Spring Boot apps

A basic repository for the Employee entity needs only one line beyond the interface declaration:

package com.example.repository;

import com.example.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

// JpaRepository<EntityType, PrimaryKeyType>
// Spring generates the implementation automatically at startup — no @Impl class needed.
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    // Inherited operations include: save, findById, findAll, deleteById, count, existsById, ...
}
// Injecting and using the repository in a service
@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    // Constructor injection — preferred over @Autowired field injection
    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public Employee hire(String fullName, String department, double salary) {
        Employee newEmployee = new Employee(fullName, department, LocalDate.now(), salary);
        return employeeRepository.save(newEmployee);  // INSERT; sets the generated id
    }

    public Employee findOrThrow(Long id) {
        // Returns Optional<Employee>; orElseThrow gives a clean 404-style error
        return employeeRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("No employee with id: " + id));
    }

    public void terminate(Long id) {
        employeeRepository.deleteById(id);  // DELETE WHERE id = ?
    }
}

4. Derived Query Methods

Spring Data JPA parses method names and generates JPQL automatically. The method name is the specification: findBy followed by the property path, optional keyword modifiers (And, Or, Not, Between, LessThan, Like, IgnoreCase, etc.), and an optional ordering suffix (OrderByX).

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // ---- Single field filters ----

    // SELECT e FROM Employee e WHERE e.department = ?1
    List<Employee> findByDepartment(String department);

    // SELECT e FROM Employee e WHERE LOWER(e.fullName) LIKE LOWER(?1)
    List<Employee> findByFullNameContainingIgnoreCase(String namePart);

    // SELECT e FROM Employee e WHERE e.salary >= ?1 ORDER BY e.salary DESC
    List<Employee> findBySalaryGreaterThanEqualOrderBySalaryDesc(double minimumSalary);

    // ---- Multi-field filters ----

    // SELECT e FROM Employee e WHERE e.department = ?1 AND e.salary BETWEEN ?2 AND ?3
    List<Employee> findByDepartmentAndSalaryBetween(
            String department, double salaryMin, double salaryMax);

    // ---- Existence and count ----

    // SELECT COUNT(e) FROM Employee e WHERE e.department = ?1
    long countByDepartment(String department);

    // SELECT COUNT(e) > 0 FROM Employee e WHERE e.fullName = ?1
    boolean existsByFullName(String fullName);

    // ---- Optional for single results ----

    // Returns Optional.empty() instead of throwing when not found
    Optional<Employee> findByFullNameIgnoreCase(String fullName);

    // ---- Return a single value with query derivation ----

    // SELECT e FROM Employee e WHERE e.hireDate < ?1 ORDER BY e.salary DESC LIMIT 1
    Optional<Employee> findFirstBySalaryLessThanOrderBySalaryDesc(double salaryThreshold);
}

5. @Query: JPQL and Native SQL

Use @Query when the derived method name would be unwieldy or when you need SQL features that JPQL cannot express (window functions, database-specific functions, complex joins).

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // --- JPQL @Query (operates on entity names and field names, not table/column names) ---

    // Named parameters: :paramName — clearer than positional ?1 for multi-param queries
    @Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary >= :minSalary ORDER BY e.salary DESC")
    List<Employee> findSeniorByDepartment(
            @Param("dept")      String department,
            @Param("minSalary") double minimumSalary);

    // Aggregate query — returns a scalar, not an entity
    @Query("SELECT AVG(e.salary) FROM Employee e WHERE e.department = :dept")
    Double averageSalaryByDepartment(@Param("dept") String department);

    // --- Native SQL @Query (uses real table and column names) ---

    // nativeQuery = true: execute raw SQL. Use when JPQL cannot express what you need.
    @Query(
        value = "SELECT * FROM employees WHERE YEAR(hire_date) = :year ORDER BY full_name",
        nativeQuery = true
    )
    List<Employee> findHiredInYear(@Param("year") int year);

    // --- Modifying @Query (UPDATE / DELETE) ---

    // @Modifying: required for any DML query (INSERT, UPDATE, DELETE).
    // @Transactional: required because modifying queries need an active transaction.
    @Modifying
    @Transactional
    @Query("UPDATE Employee e SET e.salary = e.salary * :multiplier WHERE e.department = :dept")
    int applyRaiseToDepartment(
            @Param("dept")       String department,
            @Param("multiplier") double multiplier);
    // Returns the number of rows affected
}

How the Code Works

  1. @Query accepts JPQL by default. Set nativeQuery = true to use raw SQL against the physical table and column names.
  2. Named parameters (:paramName) are bound via @Param("paramName") on the method parameter. This is preferred over positional ?1 parameters for readability.
  3. @Modifying is mandatory for any DML query (UPDATE, DELETE, INSERT). Without it, Spring throws an InvalidDataAccessApiUsageException at runtime.
  4. Modifying queries must run inside a transaction. Applying @Transactional directly on the repository method is the cleanest option when the caller may not already have a transaction.

6. Pagination and Sorting

JpaRepository inherits findAll(Pageable). Any derived query or @Query method can also accept a Pageable argument — Spring automatically appends the LIMIT/OFFSET clauses and wraps results in a Page.

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // Adding Pageable turns any query into a paginated one
    Page<Employee> findByDepartment(String department, Pageable pageable);

    // With @Query — Spring appends the pagination SQL automatically
    @Query("SELECT e FROM Employee e WHERE e.salary >= :min")
    Page<Employee> findHighEarners(@Param("min") double minimumSalary, Pageable pageable);
}
// Using Pageable in a service
@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public Page<Employee> getEngineeringPage(int pageNumber, int pageSize) {
        // PageRequest.of(pageIndex, size, sort)
        // pageIndex is 0-based: page 0 = first page
        Pageable pageable = PageRequest.of(
                pageNumber,         // 0-based page index
                pageSize,           // number of records per page
                Sort.by(Sort.Direction.DESC, "salary")  // ORDER BY salary DESC
        );
        return employeeRepository.findByDepartment("Engineering", pageable);
    }
}

// Consuming the Page result
Page<Employee> result = employeeService.getEngineeringPage(0, 10);

System.out.println("Total employees: " + result.getTotalElements()); // e.g. 47
System.out.println("Total pages:     " + result.getTotalPages());    // e.g. 5
System.out.println("Current page:    " + result.getNumber());        // 0
System.out.println("Has next page:   " + result.hasNext());          // true

// The actual employee objects for this page
List<Employee> employees = result.getContent();

7. Projections: Returning Only What You Need

Selecting the full entity when you only need two fields wastes memory and generates unnecessary SQL columns. Spring Data JPA supports two lightweight projection strategies: interface projections (a proxy-based approach) and class projections (constructor-based, works with Java Records).

Interface Projections

// Define an interface — Spring generates a proxy that reads directly from the SQL result set
public interface EmployeeNameDepartment {
    String getFullName();      // maps to the 'fullName' entity property
    String getDepartment();    // maps to the 'department' entity property
}

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    // Spring generates: SELECT e.fullName, e.department FROM Employee e WHERE ...
    // The full entity is never loaded into memory
    List<EmployeeNameDepartment> findByDepartment(String department);
}

Class (DTO) Projections — Ideal for Java Records

// A Java record is a perfect class projection — immutable, minimal, no boilerplate
public record EmployeeSummary(
        String fullName,
        String department,
        double salary
) { }

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // @Query with SELECT NEW: Hibernate calls the record's canonical constructor for each row
    @Query("""
           SELECT new com.example.dto.EmployeeSummary(
                      e.fullName, e.department, e.salary)
           FROM Employee e
           WHERE e.department = :dept
           ORDER BY e.salary DESC
           """)
    List<EmployeeSummary> findSummariesByDepartment(@Param("dept") String department);

    // Class projections also work with derived queries (Spring infers SELECT columns)
    List<EmployeeSummary> findProjectedByDepartment(String department);
    // Note: for this automatic form, the field names and constructor param names must match
}

8. Specifications: Dynamic Queries

Derived queries are static. When you need to build a query from optional runtime conditions (a search filter form with 5+ optional fields), Specification<T> is the right tool. Extend JpaSpecificationExecutor in the repository, then compose predicates using the Criteria API.

// Step 1: extend JpaSpecificationExecutor in addition to JpaRepository
@Repository
public interface EmployeeRepository
        extends JpaRepository<Employee, Long>,
                JpaSpecificationExecutor<Employee> { }
// Step 2: define reusable specification building blocks
public class EmployeeSpecifications {

    // Returns a Specification that filters by department (only applied when dept is non-null)
    public static Specification<Employee> hasDepart(String department) {
        return (root, query, criteriaBuilder) -> {
            if (department == null || department.isBlank()) {
                return criteriaBuilder.conjunction();  // "1=1" — no filter
            }
            return criteriaBuilder.equal(root.get("department"), department);
        };
    }

    // Filters employees hired on or after a minimum date
    public static Specification<Employee> hiredAfter(LocalDate minimumDate) {
        return (root, query, criteriaBuilder) -> {
            if (minimumDate == null) {
                return criteriaBuilder.conjunction();
            }
            return criteriaBuilder.greaterThanOrEqualTo(root.get("hireDate"), minimumDate);
        };
    }

    // Filters by salary range
    public static Specification<Employee> salaryBetween(Double minSalary, Double maxSalary) {
        return (root, query, criteriaBuilder) -> {
            if (minSalary == null && maxSalary == null) {
                return criteriaBuilder.conjunction();
            }
            if (minSalary == null) {
                return criteriaBuilder.lessThanOrEqualTo(root.get("salary"), maxSalary);
            }
            if (maxSalary == null) {
                return criteriaBuilder.greaterThanOrEqualTo(root.get("salary"), minSalary);
            }
            return criteriaBuilder.between(root.get("salary"), minSalary, maxSalary);
        };
    }
}

// Step 3: compose and execute
@Service
public class EmployeeSearchService {

    private final EmployeeRepository employeeRepository;

    public EmployeeSearchService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public List<Employee> search(String department, LocalDate hiredAfter,
                                  Double minSalary, Double maxSalary) {
        // Specifications are combined with .and() — null parameters are no-ops
        Specification<Employee> spec = Specification
                .where(EmployeeSpecifications.hasDepart(department))
                .and(EmployeeSpecifications.hiredAfter(hiredAfter))
                .and(EmployeeSpecifications.salaryBetween(minSalary, maxSalary));

        return employeeRepository.findAll(spec);  // executes the dynamically built query
    }
}

9. Transactions with Spring Data JPA

Spring Data JPA applies these transaction defaults: all find* methods run in a read-only transaction (enabling database-level and Hibernate-level optimisations); all write methods (save, delete) run in a read-write transaction. Service-layer methods should declare their own transaction boundaries using @Transactional.

@Service
@Transactional(readOnly = true)   // class-level default: all methods are read-only transactions
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    // Inherits read-only transaction from the class-level annotation
    public List<Employee> findAllInDepartment(String department) {
        return employeeRepository.findByDepartment(department);
    }

    // Override at method level: a write operation needs a read-write transaction
    @Transactional           // overrides the class-level readOnly = true
    public Employee giveRaise(Long employeeId, double raiseAmount) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new EntityNotFoundException("id: " + employeeId));

        // Hibernate dirty-checking detects the change and issues UPDATE automatically
        // — no explicit save() call needed when the entity is attached to the session
        employee.setSalary(employee.getSalary() + raiseAmount);
        return employee;   // the updated salary will be flushed to the DB at commit
    }

    // Both saves must succeed or both must roll back — atomicity via single transaction
    @Transactional
    public void transferEmployee(Long employeeId, String newDepartment) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new EntityNotFoundException("id: " + employeeId));
        employee.setDepartment(newDepartment);
        // Any RuntimeException here rolls back the entire transaction
    }
}

10. Auditing: @CreatedDate, @LastModifiedDate

Spring Data JPA can automatically populate creation and modification timestamps. Enable auditing once with @EnableJpaAuditing, then annotate the entity fields.

// Step 1: Enable auditing in a configuration class
@Configuration
@EnableJpaAuditing         // activates @CreatedDate, @LastModifiedDate processing
public class JpaConfig { }
// Step 2: Use @EntityListeners and the auditing annotations on entity fields
@Entity
@Table(name = "employees")
@EntityListeners(AuditingEntityListener.class)  // registers the Spring auditing listener
public class Employee {

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

    private String fullName;
    private String department;
    private double salary;

    @CreatedDate                             // set automatically on INSERT, never updated
    @Column(name = "created_at", updatable = false, nullable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate                        // updated automatically on every UPDATE
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

    protected Employee() { }

    public Employee(String fullName, String department, double salary) {
        this.fullName   = fullName;
        this.department = department;
        this.salary     = salary;
    }

    // Getters ...
    public Long          getId()         { return id; }
    public String        getFullName()   { return fullName; }
    public String        getDepartment() { return department; }
    public double        getSalary()     { return salary; }
    public LocalDateTime getCreatedAt()  { return createdAt; }
    public LocalDateTime getUpdatedAt()  { return updatedAt; }

    public void setDepartment(String department) { this.department = department; }
    public void setSalary(double salary)         { this.salary = salary; }
}

11. Testing with @DataJpaTest

Spring Boot’s @DataJpaTest slice starts only the JPA layer — the entity manager, repositories, and an in-memory database — skipping web components, security, and services. This makes repository tests fast and focused.

@DataJpaTest           // loads only JPA context; uses H2 in-memory DB by default
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)  // always use H2
class EmployeeRepositoryTest {

    @Autowired
    private EmployeeRepository employeeRepository;

    @BeforeEach
    void seedDatabase() {
        // Save test data before each test; @DataJpaTest rolls back after each test
        employeeRepository.saveAll(List.of(
            new Employee("Alice Nguyen",   "Engineering", LocalDate.of(2020, 1, 15), 95_000),
            new Employee("Bob Carpenter",  "Engineering", LocalDate.of(2019, 3, 10), 105_000),
            new Employee("Carol Jimenez",  "Marketing",   LocalDate.of(2021, 7, 1),   72_000)
        ));
    }

    @Test
    void findByDepartment_returnsOnlyMatchingEmployees() {
        List<Employee> engineers = employeeRepository.findByDepartment("Engineering");

        assertThat(engineers).hasSize(2);
        assertThat(engineers).extracting(Employee::getFullName)
                              .containsExactlyInAnyOrder("Alice Nguyen", "Bob Carpenter");
    }

    @Test
    void findBySalaryGreaterThanEqualOrderBySalaryDesc_returnsSortedResults() {
        List<Employee> highEarners = employeeRepository
                .findBySalaryGreaterThanEqualOrderBySalaryDesc(90_000.0);

        assertThat(highEarners).hasSize(2);
        // Verify descending order
        assertThat(highEarners.get(0).getSalary()).isGreaterThan(highEarners.get(1).getSalary());
    }

    @Test
    void countByDepartment_returnsCorrectCount() {
        long engineeringCount = employeeRepository.countByDepartment("Engineering");
        assertThat(engineeringCount).isEqualTo(2);
    }
}

12. Choosing the Right Query Strategy

ScenarioBest approach
Simple equality filter on 1–2 fieldsDerived query method
Complex filter, multi-field, readable@Query JPQL with named parameters
Database-specific function / window query@Query(nativeQuery = true)
Optional/dynamic filters from a UI formSpecification<T> + JpaSpecificationExecutor
Need only a subset of entity fieldsInterface projection or record class projection
Need total count + paginated dataDerived or @Query method with Pageable return type Page<T>
Bulk UPDATE / DELETE@Modifying + @Query + @Transactional
Audit timestamps without manual code@EnableJpaAuditing + @CreatedDate / @LastModifiedDate

See Also

Conclusion

Spring Data JPA sits at the intersection of Spring Boot and Hibernate, giving you a type-safe, low-boilerplate data-access layer that scales from a single-table CRUD app to a multi-entity production service. Start with derived query methods for simple filters, reach for @Query when you need full control over the JPQL or SQL, use Specifications when filters are dynamic, and lean on projections whenever you do not need the full entity. Declare transaction boundaries at the service layer with @Transactional(readOnly = true) as the class default and override to read-write only where writes occur — this gives Hibernate the information it needs to skip dirty-checking on read paths and keeps your transactions explicit and reviewable.


Frequently Asked Questions

What is the difference between Spring Data JPA and Hibernate?

Hibernate is a JPA implementation — it does the actual work of translating objects to SQL. Spring Data JPA is a higher-level abstraction that sits on top of JPA (and by extension Hibernate). It generates repository implementations automatically and provides utilities like derived query methods, Pageable, Specifications, and auditing. You still depend on Hibernate under the hood; Spring Data JPA just removes the boilerplate of writing EntityManager code by hand.

Should I use save() or saveAndFlush()?

save() persists or merges the entity and defers the SQL INSERT/UPDATE to the flush boundary (usually transaction commit). saveAndFlush() forces the SQL to execute immediately. Use save() in the vast majority of cases; use saveAndFlush() only when you need the database to see the change within the same transaction before it commits — for example, before calling a stored procedure that reads the same row.

When should I use findById() vs getReferenceById()?

findById() immediately issues a SELECT and returns Optional<T> — use this when you actually need the entity data. getReferenceById() returns a Hibernate proxy without hitting the database — use this when you only need a reference to set as a foreign key on another entity, avoiding an unnecessary SELECT. Accessing any field on the proxy (other than the @Id) triggers a lazy load.

Can I use @Query with a Java Record return type?

Yes. Use the SELECT new com.example.dto.MyRecord(e.field1, e.field2) FROM Entity e JPQL syntax. Hibernate will call the record’s canonical constructor for each result row. The argument order in SELECT new must exactly match the record component declaration order. This is the recommended approach for read-only summary projections.

What happens if a @Transactional method calls another @Transactional method in the same class?

Spring’s @Transactional is implemented via a proxy. A method calling another method on this bypasses the proxy, so the inner method’s @Transactional annotation is ignored. Both methods share the outer transaction. To enforce independent transaction behaviour, the called method must be on a different Spring bean so the proxy is invoked.

AI Prompts for Spring Data JPA

Use these prompts in Claude, ChatGPT, or Gemini to speed up your Spring Data JPA work:

  • “Generate a JpaRepository interface for this entity with derived query methods for: filter by status, find by email ignoring case, count by category, and find all ordered by createdAt desc: [paste entity]”
  • “Convert this raw JPQL @Query to a Specification<T> that accepts optional parameters so callers can pass null to skip any filter: [paste query]”
  • “Write a @DataJpaTest class for this repository with @BeforeEach seed data and test methods for every custom query: [paste repository interface]”
  • “Add @CreatedDate, @LastModifiedDate, and @CreatedBy auditing to this JPA entity and show the required configuration class: [paste entity]”
  • “Design a record-based DTO projection for this entity returning only id, name, and status, and write the @Query that populates it: [paste entity]”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.