Spring Boot JPA + CRUD
- What is Spring Data JPA? How does it simplify database access?
- What is the role of JpaRepository interface?
- What is @Entity annotation?
- @Id and @GeneratedValue – explain strategies
- How to create custom query methods in repository?
- @Query annotation – JPQL vs Native SQL
- What is pagination in Spring Data?
- How to use Pageable in controller?
- What is @Transactional? When to use?
- Difference between persist() and save() (JPA vs Spring Data)
- Entity relationships basics (@OneToMany, @ManyToOne)
- How to handle validation in entities/DTOs?
- Global exception handling with @ControllerAdvice
- Common pitfalls of Spring Boot JPA + CRUD
- What are the best practices in Spring Boot JPA + CRUD ?
What is Spring Data JPA? How does it simplify database access?
=> Spring Data JPA is a Spring Module that simplifies the implementation of JPA repository
=> We can define an Interface extending JpaRepository. From that Spring generates the implementation at runtime using proxies.
=> It provides CRUD methods automatically, supports query derivation from method names, pagination and custom @Query
=> Compared to plain JPA, it eliminates boilerplate DAO classes, manual EntityManager handling, and repetitive code i.e we can focus on business logic instead of persistence details
=> Example : Traditional (Without Spring Data JPA) vs Spring Data JPA
Traditional (Without Spring Data JPA)
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
@PersistenceContext
private EntityManager em;
public List<Employee> findByDepartment(String dept) {
return em.createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
.setParameter("dept", dept)
.getResultList();
}
// ... save, delete, etc. — many methods
}
public class EmployeeDaoImpl implements EmployeeDao {
@PersistenceContext
private EntityManager em;
public List<Employee> findByDepartment(String dept) {
return em.createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
.setParameter("dept", dept)
.getResultList();
}
// ... save, delete, etc. — many methods
}
With Spring Data JPA
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByDepartment(String department); // Auto-implemented!
}
List<Employee> findByDepartment(String department); // Auto-implemented!
}
JpaRepository<Employee, Long> means
Employee - The Entity class (first parameter T)
Long - The type of the primary key (second parameter ID)
Common Repository Interfaces
=> CrudRepository<T, ID> — basic CRUD
=> PagingAndSortingRepository<T, ID> — adds pagination/sorting
=> JpaRepository<T, ID> — most used (extends above + flush, deleteInBatch, etc.)
Best Practices
=> Use Interfaces only -- No implementation needed
=> Prefer method name derivation for simple queries
=> Use @Query for complex queries
=> Always use DTOs in service/controller, never expose entities directly
What is the role of JpaRepository interface?
=> JpaRepository is a Spring Data JPA interface extends CRUDRepository and PagingAndSortingRepository.
=> It provides ready use to CRUD methods, pagination, sorting and jpa specific methods like flush and deleteInBatch
=> We just define an interface extending JpaRepository. From that Spring generates the implementation at runtime using proxies.
=> Compared to plain JPA, it eliminates boilerplate DAO classes, manual EntityManager handling, and repetitive code i.e we can focus on business logic instead of persistence details
=> JpaRepository is the most commonly used repository interface in Spring Boot applications
=> JPA - Java Persistence API, CRUD - Create Read Update Delete
JpaRepository Hierarchy (The Full Inheritance Chain of JpaRepository)
JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T>
PagingAndSortingRepository<T, ID>
extends CrudRepository<T, ID>
CrudRepository<T, ID>
extends Repository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T>
PagingAndSortingRepository<T, ID>
extends CrudRepository<T, ID>
CrudRepository<T, ID>
extends Repository<T, ID>
=> So the complete chain is JpaRepository → PagingAndSortingRepository → CrudRepository → Repository
Key Methods Provided by JpaRepository (Most Frequently Used)
| Method | Description | Example Use Case |
|---|---|---|
save(S entity) | Save or update an entity | Insert new employee or update existing |
saveAll(Iterable<S> entities) | Save multiple entities | Bulk insert |
findById(ID id) | Return Optional<T> for entity by ID | Get employee by ID |
existsById(ID id) | Check if entity exists | Validation before delete |
findAll() | Return all entities | List all employees |
findAll(Sort sort) | Return all with sorting | Sort by salary descending |
findAll(Pageable pageable) | Return paginated result (Page<T>) | Pagination in UI (page 1, size 10) |
count() | Return total number of entities | Show total records |
delete(T entity) / deleteById(ID id) | Delete entity | Remove employee |
deleteAll() | Delete all entities | Cleanup (rare in production) |
flush() | Synchronize persistence context with DB | Force immediate write |
saveAndFlush(S entity) | Save and immediately flush | Ensure ID available right after save |
What Each Interface Adds
| Interface | Key Methods Added | Purpose |
|---|---|---|
| Repository<T, ID> | Marker interface (no methods) | Base marker |
| CrudRepository<T, ID> | save(), findById(), findAll(), delete(), count(), etc. | Basic CRUD |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) | Pagination + Sorting |
| QueryByExampleExecutor | findOne(), findAll(), count(), exists() by example | Query by example |
| JpaRepository | flush(), saveAndFlush(), deleteInBatch(), deleteAllInBatch() | JPA-specific (flush, batch operations) |
=> CrudRepository<T, ID> → basic CRUD (save, findById, findAll, delete, etc.)
=> PagingAndSortingRepository<T, ID> → adds findAll(Pageable) and findAll(Sort)
=> QueryByExampleExecutor<T> → adds query-by-example methods
=> JpaRepository adds JPA-specific methods like flush(), saveAndFlush(), deleteInBatch(), etc
Example in Practice
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// No need to implement anything — all methods above are available!
}
// Usage in Service
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository repo;
public Page<Employee> getEmployees(Pageable pageable) {
return repo.findAll(pageable); // Pagination + sorting
}
public Employee createEmployee(Employee emp) {
return repo.save(emp); // Insert
}
}
// No need to implement anything — all methods above are available!
}
// Usage in Service
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository repo;
public Page<Employee> getEmployees(Pageable pageable) {
return repo.findAll(pageable); // Pagination + sorting
}
public Employee createEmployee(Employee emp) {
return repo.save(emp); // Insert
}
}
Best Practices
=> Use JpaRepository for most cases (covers 90% of needs)
=> Extend CrudRepository only if you want minimal methods (no pagination)
=> Use PagingAndSortingRepository if you need pagination but not JPA-specific methods
What is @Entity annotation?
=> @Entity marks a class as a JPA entity
=> It tells JPA provider (Usually Hibernate in Spring Boot) that this class should be mapped to a database table
=> The annotation @Entity applied at class level
=> The entity class must have no-arg constructor (can be private) and usually a primary key marked with @Id
=> By default, table name = class name, column names = field names
=> We can use @Table, @Column for customization
=> It is a part of the jakarta.persistence (or javax.persistence for older versions) package
=> It used to enable ORM (Object Relational Mapping)
Example
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@Entity // This makes the class an entity → mapped to table "employee" if no custom table name provided
@Table(name = "employees") // Custom table name
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@Entity // This makes the class an entity → mapped to table "employee" if no custom table name provided
@Table(name = "employees") // Custom table name
public class Employee {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-increment
@Column(name = "emp_id") // Custom column name
private Long id;
private String name;
private int salary;
// No-arg constructor (required by JPA)
public Employee() {}
public Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
// Getters and setters...
}
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-increment
@Column(name = "emp_id") // Custom column name
private Long id;
private String name;
private int salary;
// No-arg constructor (required by JPA)
public Employee() {}
public Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
// Getters and setters...
}
Rules and Requirements
=> Must have no-arg constructor (JPA uses reflection)
=> Fields are usually private with getters/setters
=> Should be non final (for proxying in lazy loading)
=> Can have relationships (@OneToMany, @ManyToOne, etc)
Best Practices
=> Never expose entities directly in REST APIs — use DTOs (security, avoid lazy loading issues).
=> Use @NotNull, @Size, etc. from Bean Validation for constraints
=> Keep entities simple — only data + minimal logic
@Id and @GeneratedValue – explain strategies
=> Annotation @Id marks a field or property as the primary key of the entity
=> Every JPA entity must have exactly one @Id
=> It can be applied at field or getter method level
=> It's type is usually Long, Integer, String or UUID
=> Annotation @GeneratedValue tells JPA how to automatically generate the primary key value when saving the new entity
=> @GeneratedValue always paired with @Id
=> If we omit the @GeneratedValue, then we need to set the Id manually
Generation strategies
- GenerationType.IDENTITY
- GenerationType.SEQUENCE
- GenerationType.TABLE
- GenerationType.AUTO
| Strategy | Description | When to Use | Example / Notes |
|---|---|---|---|
| GenerationType.IDENTITY | Uses database auto-increment column (e.g., IDENTITY in MySQL, SERIAL in PostgreSQL). JPA retrieves generated ID after insert. | Most common for MySQL, PostgreSQL, SQL Server | Simple, performant. No sequence needed. |
| GenerationType.SEQUENCE | Uses database sequence object (e.g., Oracle, PostgreSQL). JPA calls sequence before insert. | Oracle (default), PostgreSQL with custom sequence | More control (allocationSize, pre-allocation). |
| GenerationType.TABLE | Uses a database table to simulate sequence (portable but slow). | When no native support (rare now) | Poor performance — avoid in production. |
| GenerationType.AUTO | JPA chooses the best strategy based on database dialect (default). | Quick prototyping, let JPA decide | Often maps to IDENTITY or SEQUENCE. |
Detailed example with these 4 strategies
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // MySQL/PostgreSQL auto-increment
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_seq")
@SequenceGenerator(name = "emp_seq", sequenceName = "employee_seq", allocationSize = 50)
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "emp_table")
@TableGenerator(name = "emp_table", table = "id_generator", pkColumnValue = "employee")
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // Let JPA decide
private Long id;
private String name;
}
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // MySQL/PostgreSQL auto-increment
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_seq")
@SequenceGenerator(name = "emp_seq", sequenceName = "employee_seq", allocationSize = 50)
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "emp_table")
@TableGenerator(name = "emp_table", table = "id_generator", pkColumnValue = "employee")
private Long id;
// or
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // Let JPA decide
private Long id;
private String name;
}
Best Practice
=> Use GenerationType.IDENTITY for most cases (MySQL, PostgreSQL, H2)
=> Use GenerationType.SEQUENCE for Oracle or when you want allocationSize > 1 (batch inserts)
=> Avoid TABLE — slow and outdated
=> Avoid AUTO in production — unpredictable across databases
How can we use @Id with getter method ?
Example
@Entity
public class Employee {
private Long id;
private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
public class Employee {
private Long id;
private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
=> Use @Id on field — simpler, clearer, most common.
=> Use getter only if you have a specific reason (When you want to add logic in getter (rare for ID))
=> If you place @Id on getter but have other annotations on fields, JPA might get confused. To be safe, explicitly tell JPA to use property access
=> Example :
@Entity
@Access(AccessType.PROPERTY) // Important when @Id is on getter
public class Employee {
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
}
@Access(AccessType.PROPERTY) // Important when @Id is on getter
public class Employee {
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
}
How to create custom query methods in repository?
=> We can create custom query methods in Spring Data JPA in the following two ways :
- Query Method Derivation (Naming Convention – No @Query Needed)
- Custom Queries Using @Query Annotation (When Naming Convention Is Not Enough)
1. Query Method Derivation (Naming Convention – No @Query Needed)
=> Spring Data JPA parses the method name and creates the query automatically
=> Rules for Method Names:
Start with: find…By, read…By, query…By, count…By, exists…By, delete…By.
Keywords: And, Or, Is, Equals, Between, LessThan, GreaterThan, Like, NotLike, In, NotIn, OrderBy, First, Top.
=> Examples :
| Method Signature | Generated JPQL (Approximate) | Use Case |
|---|---|---|
findByName(String name) | SELECT e FROM Employee e WHERE e.name = ?1 | Find by exact name |
findBySalaryGreaterThan(int salary) | SELECT e FROM Employee e WHERE e.salary > ?1 | Salary > given |
findByDepartmentAndSalaryGreaterThan(String dept, int salary) | SELECT e FROM Employee e WHERE e.department = ?1 AND e.salary > ?2 | Combined conditions |
findByNameContaining(String keyword) | SELECT e FROM Employee e WHERE e.name LIKE %?1% | Search containing |
findBySalaryBetween(int min, int max) | SELECT e FROM Employee e WHERE e.salary BETWEEN ?1 AND ?2 | Range query |
findByNameIsNull() | SELECT e FROM Employee e WHERE e.name IS NULL | Null check |
findByDepartmentOrderBySalaryDesc(String dept) | SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESC | Sorted results |
findFirstByDepartmentOrderBySalaryDesc(String dept) | SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESC LIMIT 1 | First/top result |
findTop3ByDepartmentOrderBySalaryDesc(String dept) | SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESC LIMIT 3 | Top N results |
2. Custom Queries Using @Query Annotation (When Naming Convention Is Not Enough)
=> When method name derivation is not sufficient (complex joins, subqueries, native SQL), use @Query
JPQL Example (JPQL - Java Persistence Query Language)
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
Native SQL Example
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
Best Practices
=> Prefer method name derivation for simple queries — readable and maintainable
=> Use @Query only for complex queries or native SQL
=> Always use @Param for named parameters in @Query
=> Avoid native queries unless necessary (JPQL is portable across databases)
@Query annotation – JPQL vs Native SQL
Difference Between JPQL and Native SQL in Spring Data JPA
| Aspect | JPQL (Java Persistence Query Language) | Native SQL |
|---|---|---|
| Language Type | Object-oriented — queries are written against entity classes and their properties (not database tables/columns). | Database-specific — direct SQL against actual tables and columns. |
| Portability | Portable across databases — same JPQL works on MySQL, PostgreSQL, Oracle, etc. (JPA provider translates to dialect). | Not portable — tied to specific database (syntax, functions differ). |
| Syntax Example | SELECT e FROM Employee e WHERE e.salary > :salary | SELECT * FROM employee WHERE salary > ? |
| Entity Mapping | Uses entity names (Employee) and property names (salary) — matches Java class. | Uses table names (employee) and column names (salary). |
| Result Type | Returns managed entities or projections (can be DTOs with SELECT new). | Returns raw data (Object[], Map, or custom result with @SqlResultSetMapping). |
| How to Use in Repository | Default for method name derivation and @Query without nativeQuery=true. | Use @Query(nativeQuery = true) |
| Performance | Slight overhead (translation to SQL), but usually negligible. | Direct — potentially faster for complex queries. |
| Database-Specific Features | Limited — cannot use vendor-specific functions (e.g., MySQL JSON_EXTRACT). | Full access — use any database function, hint, etc. |
| Security | Parameters are bound safely — better protection against SQL injection. | Same if using parameters (? or :param), but risk if concatenating strings. |
| When to Use | Most cases — business logic, CRUD, when portability matters. | Complex reports, database-specific features, performance-critical queries. |
JPQL Example (JPQL - Java Persistence Query Language)
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
Native SQL Example
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary);
What is pagination in Spring Data?
=> Pagination is a mechanism to retrieve large data in small manageable chunks (pages) instead of loading everything at once
=> It is essential for performance, better user experience in applications displaying lists
Core Intefaces and Classes
1. Pageable - Represents pagination information (page number, page size, sort)
2. Page<T> - Return Type containing :
List of entities for current page (getContent())
Total pages (getTotalPages())
Total elements (getTotalElements())
Current page number, size, etc
3. Slice<T> - No count query. Only knows if next slice exists (faster for larger/infinite dataset)
Best Practices
=> Always use pagination for list endpoints
=> Default reasonable size (10-20)
=> Use Slice when total count not needed
=> Combine with @PageableDefault annotation for defaults
Example
//Repository method
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// Returns a Page of employees with pagination + optional sorting
Page<Employee> findAll(Pageable pageable);
// Custom query with pagination
Page<Employee> findByDepartment(String department, Pageable pageable);
}
// Returns a Page of employees with pagination + optional sorting
Page<Employee> findAll(Pageable pageable);
// Custom query with pagination
Page<Employee> findByDepartment(String department, Pageable pageable);
}
//Controller usage
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeRepository repo;
@GetMapping
public Page<Employee> getEmployees(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
Sort sort = sortDir.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return repo.findAll(pageable);
}
}
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeRepository repo;
@GetMapping
public Page<Employee> getEmployees(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
Sort sort = sortDir.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return repo.findAll(pageable);
}
}
URL examples
http://localhost:8080/employees?page=0&size=10&sortBy=salary&sortDir=desc
(Page 0 (first), 10 items, sorted by salary descending)
Response JSON snippet
{
"content": [/* 10 employees */],
"pageable": { "pageNumber": 0, "pageSize": 10, ... },
"totalPages": 25,
"totalElements": 250,
"last": false,
"numberOfElements": 10
}
"content": [/* 10 employees */],
"pageable": { "pageNumber": 0, "pageSize": 10, ... },
"totalPages": 25,
"totalElements": 250,
"last": false,
"numberOfElements": 10
}
How to use Pageable in controller?
=> Pageable is a Spring Data Interface represents Pagination information (page number, page size, sort)
=> Example :
@GetMapping("/employees")
public Page<Employee> getEmployees(Pageable pageable) {
return employeeRepository.findAll(pageable);
}
public Page<Employee> getEmployees(Pageable pageable) {
return employeeRepository.findAll(pageable);
}
=> When you add a Pageable parameter to a controller method, Spring Boot automatically binds request parameters like page, size, sort to it
| Parameter | Default Value | |
|---|---|---|
page | 0 | |
size | 20 | |
sort | none |
=> URL Examples :
http://localhost:8080/employees
=> page 0, size 20, no sort (default)
http://localhost:8080/employees?page=2&size=10
=> page 2, 10 items per page
http://localhost:8080/employees?sort=salary,desc
=> sorted by salary descending
http://localhost:8080/employees?page=1&size=5&sort=name,asc&sort=salary,desc
=> page 1, size 5, sort by name ascending then salary descending
=> page 0, size 20, no sort (default)
http://localhost:8080/employees?page=2&size=10
=> page 2, 10 items per page
http://localhost:8080/employees?sort=salary,desc
=> sorted by salary descending
http://localhost:8080/employees?page=1&size=5&sort=name,asc&sort=salary,desc
=> page 1, size 5, sort by name ascending then salary descending
Complete Example
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Page<Employee> findAll(Pageable pageable);
Page<Employee> findByDepartment(String department, Pageable pageable);
}
Page<Employee> findAll(Pageable pageable);
Page<Employee> findByDepartment(String department, Pageable pageable);
}
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeRepository employeeRepository;
@GetMapping
public Page<Employee> getAllEmployees(Pageable pageable) {
return employeeRepository.findAll(pageable);
}
// With custom query
@GetMapping("/search")
public Page<Employee> searchByDepartment(
@RequestParam String department,
Pageable pageable) {
return employeeRepository.findByDepartment(department, pageable);
}
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeRepository employeeRepository;
@GetMapping
public Page<Employee> getAllEmployees(Pageable pageable) {
return employeeRepository.findAll(pageable);
}
// With custom query
@GetMapping("/search")
public Page<Employee> searchByDepartment(
@RequestParam String department,
Pageable pageable) {
return employeeRepository.findByDepartment(department, pageable);
}
//Customizing Defaults with @PageableDefault
@GetMapping
public Page<Employee> getEmployees(
@PageableDefault(page = 0, size = 50, sort = "salary", direction = Sort.Direction.DESC)
Pageable pageable) {
return employeeRepository.findAll(pageable);
}
}
Response (JSON from Page<T>)
{
"content": [/* list of employees */],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {"sorted": true, "unsorted": false, "empty": false},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalPages": 15,
"totalElements": 150,
"last": false,
"numberOfElements": 10,
"size": 10,
"number": 0,
"sort": {"sorted": true, "unsorted": false, "empty": false},
"first": true,
"empty": false
}
"content": [/* list of employees */],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {"sorted": true, "unsorted": false, "empty": false},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalPages": 15,
"totalElements": 150,
"last": false,
"numberOfElements": 10,
"size": 10,
"number": 0,
"sort": {"sorted": true, "unsorted": false, "empty": false},
"first": true,
"empty": false
}
What is @Transactional? When to use?
=> It tells Spring to automatically manage database transactions
=> When a method annotated with @Transactional is called, Spring :
Starts a transaction before the method begins
Commits the transaction if the method completes successfully
Rolls back the transaction if an unchecked exception (runTimeException or error) is thrown
How It Works (Behind the Scenes)
=> Spring uses AOP (Aspect-Oriented Programming) — creates a proxy around the bean
=> The proxy intercepts method calls and manages the transaction using a PlatformTransactionManager (e.g., DataSourceTransactionManager for JDBC/JPA).
Basic syntax
@Service
public class EmployeeService {
@Transactional // Applies to all methods
public class EmployeeService { ... }
// Or on individual method
@Transactional
public void transferSalary(Long fromId, Long toId, BigDecimal amount) {
// Multiple DB operations — all in one transaction
accountRepo.debit(fromId, amount);
accountRepo.credit(toId, amount);
}
}
public class EmployeeService {
@Transactional // Applies to all methods
public class EmployeeService { ... }
// Or on individual method
@Transactional
public void transferSalary(Long fromId, Long toId, BigDecimal amount) {
// Multiple DB operations — all in one transaction
accountRepo.debit(fromId, amount);
accountRepo.credit(toId, amount);
}
}
Key Attributes of @Transactional
| Attribute | Default | Description |
|---|---|---|
propagation | REQUIRED | How transaction propagates (REQUIRED, REQUIRES_NEW, NESTED, etc.) |
isolation | DEFAULT | Isolation level (READ_UNCOMMITTED, READ_COMMITTED, etc.) |
timeout | -1 (no timeout) | Transaction timeout in seconds |
readOnly | false | Hint for read-only (optimizes for reads) |
rollbackFor | unchecked exceptions | Specify which exceptions trigger rollback (default: RuntimeException/Error) |
noRollbackFor | none | Exceptions that do not trigger rollback |
When to Use @Transactional
=> Use it whenever you have multiple database operations that must succeed or fail together.
=> Example : Multiple writes (transfer money, order processing)
@Transactional
public void placeOrder(Order order) {
orderRepo.save(order);
inventoryRepo.reduceStock(order.getItems());
paymentRepo.charge(order.getAmount());
}
public void placeOrder(Order order) {
orderRepo.save(order);
inventoryRepo.reduceStock(order.getItems());
paymentRepo.charge(order.getAmount());
}
=> All or nothing (eg. if payment fails, stock and order are rolled back)
=> Service layer is the best place to have @Transactional (not in controller or repository)
=> Read-only optimization:
@Transactional(readOnly = true)
public List<Employee> getAllEmployees() { ... }
public List<Employee> getAllEmployees() { ... }
When NOT to Use
=> Single read operation (no need for transaction overhead)
=> Non-database operations
=> Common Pitfall: Proxy Limitation
Example :
@Service
public class EmployeeService {
@Transactional
public void methodA() { ... }
public void methodB() {
methodA(); // No transaction! Called directly — not through proxy
}
}
public class EmployeeService {
@Transactional
public void methodA() { ... }
public void methodB() {
methodA(); // No transaction! Called directly — not through proxy
}
}
=> In the above example, the calling methodA inside methodB breaks the proxy. So avoid self calls
What does proxy mean here?
=> In the context of Spring Data JPA (and Spring in general), a proxy is a dynamically generated subclass created at runtime that "wraps" your original bean (e.g., repository, service, or entity) to add extra behavior without changing the original code
=> Remember the behavior of JpaRepository (Spring Data JPA)
=> Example :
public interface EmployeeRepository extends JpaRepository<Employee, Long> { }
=> You write only the interface — no implementation.
=> Spring creates a proxy at runtime that implements this interface.
=> When you call employeeRepository.findById(1L), the proxy generates and executes the SQL behind the scenes.
=> You never see the real implementation class — it's all proxy magic.
Difference between persist() and save() (JPA vs Spring Data)
=> persist() is a pure JPA(EntityManager) method
=> save() is a Spring Data JPA(CRUD repository/JPA repository) method
=> persist() schedules insert, returns void, does nothing on existing entity
=> save() returns saved entity with generated ID, handles both insert (new) and merge (existing)
Code Example (Shows the Difference)
Employee emp = new Employee();
emp.setName("Sachin");
// JPA way (EntityManager)
entityManager.persist(emp); // OK — new entity
entityManager.persist(emp); // Second call — does nothing (no error, but no update)
// Spring Data JPA way
Employee saved1 = repo.save(emp); // Insert — returns entity with generated ID
saved1.setName("Sachin Tendulkar");
Employee saved2 = repo.save(saved1); // Update — returns updated entity
emp.setName("Sachin");
// JPA way (EntityManager)
entityManager.persist(emp); // OK — new entity
entityManager.persist(emp); // Second call — does nothing (no error, but no update)
// Spring Data JPA way
Employee saved1 = repo.save(emp); // Insert — returns entity with generated ID
saved1.setName("Sachin Tendulkar");
Employee saved2 = repo.save(saved1); // Update — returns updated entity
| Feature | EntityManager.persist() (JPA) | repository.save() (Spring Data JPA) |
|---|---|---|
| Return value | void | Returns saved entity (with generated ID) |
| New entity (no ID) | Inserts | Inserts |
| Existing entity (has ID) | Does nothing (no update) | Updates (merge) |
| Immediate DB insert | No (only on flush/commit) | No (on flush/commit) |
| Typical usage | Low-level JPA code | Standard in Spring Boot |
| Error on existing entity | May throw EntityExistsException | No error — updates |
| Convenience | Low | High — handles both insert and update |
Entity relationships basics (@OneToMany, @ManyToOne)
| Aspect | @OneToMany | @ManyToOne |
|---|---|---|
| Meaning | One entity instance is associated with multiple instances of another entity. | Many entity instances are associated with one instance of another entity. |
| Ownership | Non-owning side (foreign key is in the target entity table). | Owning side (foreign key column is in this entity's table). |
| Typical Example | One Department has many Employees. | Many Employees belong to one Department. |
| Annotation Placement | On the collection field in the parent entity (one side). | On the reference field in the child entity (many side). |
| Field Type | Collection (List, Set) e.g., List<Employee> employees | Single reference e.g., Department department |
| Default Fetch Type | LAZY | EAGER |
| mappedBy | Required — specifies the owning side field name. | Not used |
| Cascade | Often used (e.g., CascadeType.ALL) to propagate operations to children. | Less common — usually no cascade from child to parent. |
| Orphan Removal | Often used with @OneToMany to delete orphaned children. | Not applicable. |
| Join Column | Usually not needed (placed on owning side). | Use @JoinColumn to customize foreign key column name. |
| Bidirectional? | Common — pair with @ManyToOne on child side. | Common — reference back to parent. |
| Database Impact | No foreign key in parent table. | Foreign key column in child table. |
Key points
=> @OneToMany is non-owning ie foreign key is in the target entity table
=> mappedBy is mandatory on @OneToMany to avoid duplicate FK columns.
=> @ManyToOne is Owning side (foreign key column is in this entity's table).
Code Example (Bidirectional relationship)
@Entity
public class Department {
@Id
@GeneratedValue
private Long id;
private String name;
// One Department → Many Employees
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
// getters/setters
}
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
// Many Employees → One Department (owning side)
@ManyToOne
@JoinColumn(name = "dept_id") // Optional — custom FK column name
private Department department;
// getters/setters
}
public class Department {
@Id
@GeneratedValue
private Long id;
private String name;
// One Department → Many Employees
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
// getters/setters
}
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
// Many Employees → One Department (owning side)
@ManyToOne
@JoinColumn(name = "dept_id") // Optional — custom FK column name
private Department department;
// getters/setters
}
Database Tables
department table: id, name
employee table: id, name, dept_id (foreign key → department.id)
How to handle validation in entities/DTOs?
=> In Spring Boot applications, validation is primarily handled using Bean Validation API (Jakarta validation)
=> Best practice is keeping the validation on DTOs and keeping the entities clean
=> Use Validation annotations such as @Valid, @NotBlank, etc
=> Use @Valid on controller paratmeters to trigger validations
=> Handle errors globally with @ControllerAdvice and MethodArgumentNotValidException for custom JSON responses
=> Avoid validation on entities to maintain separation of concerns
Best Practice: Validate on DTOs (Not Entities), Why?
=> DTOs (Data Transfer Objects): Plain classes used only for API input/output
=> Separation of concerns: Entities are for persistence (JPA), DTOs for API contract
=> Different API versions can have different validation
=> Prevent accidental exposure of entity constraints in JSON
Validation Annotations (Most Common)
| Annotation | Package | Example Use | Meaning |
|---|---|---|---|
@NotNull | constraints | @NotNull private String name; | Cannot be null |
@NotEmpty | constraints | @NotEmpty private String name; | Not null and not empty (for collections/strings) |
@NotBlank | constraints | @NotBlank private String name; | Not null, not empty, not whitespace-only (strings) |
@Size(min=, max=) | constraints | @Size(min=2, max=50) private String name; | Length between min–max |
@Min/@Max | constraints | @Min(18) private int age; | Minimum/maximum value |
@Positive / @PositiveOrZero | constraints | @Positive private int salary; | >0 or ≥0 |
@Email | constraints | @Email private String email; | Valid email format |
@Pattern(regexp=) | constraints | @Pattern(regexp="\\d{10}") private String phone; | Matches regex |
@Valid | constraints | @Valid @RequestBody EmployeeDto dto | Trigger nested object validation |
Example : Step-by-Step: Validation on DTOs (Recommended Way)
1. Create DTO
public class EmployeeRequestDto {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotNull(message = "Salary cannot be null")
@Positive(message = "Salary must be positive")
private Integer salary;
@Email(message = "Invalid email format")
private String email;
// getters and setters
}
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotNull(message = "Salary cannot be null")
@Positive(message = "Salary must be positive")
private Integer salary;
@Email(message = "Invalid email format")
private String email;
// getters and setters
}
2. Use @Valid in Controller
@PostMapping("/employees")
public ResponseEntity<Employee> create(@Valid @RequestBody EmployeeRequestDto dto) {
Employee employee = employeeService.create(dto);
return ResponseEntity.status(201).body(employee);
}
public ResponseEntity<Employee> create(@Valid @RequestBody EmployeeRequestDto dto) {
Employee employee = employeeService.create(dto);
return ResponseEntity.status(201).body(employee);
}
3. Automatic Validation
=> Spring sees @Valid → validates the DTO using Bean Validation.
=> If invalid → throws MethodArgumentNotValidException.
4. Handle Validation Errors Globally (@ControllerAdvice)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
Error Response Example:
{
"name": "Name is required",
"salary": "Salary must be positive"
}
"name": "Name is required",
"salary": "Salary must be positive"
}
Global exception handling with @ControllerAdvice
=> @ControllerAdvice is a specialized @Component annotation that allows you to handle exceptions globally across all @Controller or @RestController classes in your Spring Boot application
=> One centralized place for all exception handling logic
=> Uniform error format (e.g., JSON with error code, message, timestamp)
=> Separation of Concerns: Controllers stay clean — focus on business logic, not error handling
=> Reusable ie Works across all controllers
How It Works
=> Spring scans for classes annotated with @ControllerAdvice
=> Methods annotated with @ExceptionHandler inside it are invoked when the specified exception is thrown from any controller.
Basic Example – Global Exception Handler
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody (for REST)
public class GlobalExceptionHandler {
// Handle specific exception
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Handle validation errors (from @Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
// Fallback for any other exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Unexpected error: " + ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Simple error response DTO
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody (for REST)
public class GlobalExceptionHandler {
// Handle specific exception
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Handle validation errors (from @Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
// Fallback for any other exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Unexpected error: " + ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Simple error response DTO
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
Common Exceptions Handled
| Exception Type | Typical Status | Use Case |
|---|---|---|
ResourceNotFoundException | 404 | Entity not found |
MethodArgumentNotValidException | 400 | @Valid validation failures |
ConstraintViolationException | 400 | @Valid on method parameters |
DataIntegrityViolationException | 400/409 | DB constraint violation |
AccessDeniedException (Security) | 403 | Authorization failure |
Exception (fallback) | 500 | Unexpected errors |
Advanced Features
=> @Order — control precedence if multiple @ControllerAdvice.
=> @ExceptionHandler with multiple exceptions:
@ExceptionHandler({ResourceNotFoundException.class, IllegalArgumentException.class})
Best Practice: Place GlobalExceptionHandler in a separate package, not inside Model, Service, or Controller layers.
com.example.demo
├── controller → Your @RestController classes
├── service → Business logic
├── repository → JPA repositories
├── model/entity → @Entity classes and DTOs
├── exception → Custom exceptions (e.g., ResourceNotFoundException)
└── config → Configuration classes
└── GlobalExceptionHandler.java
├── controller → Your @RestController classes
├── service → Business logic
├── repository → JPA repositories
├── model/entity → @Entity classes and DTOs
├── exception → Custom exceptions (e.g., ResourceNotFoundException)
└── config → Configuration classes
└── GlobalExceptionHandler.java
Common pitfalls of Spring Boot JPA + CRUD
| # | Pitfall | Description & Why It Happens | Symptoms / Errors | How to Avoid / Fix |
|---|---|---|---|---|
| 1 | N+1 Select Problem | Lazy-loaded associations (@OneToMany, @ManyToOne default LAZY) cause separate query for each entity when accessed in a loop. | Slow performance, many DB queries (e.g., 1 + N queries). | Use fetch join in custom @Query, EntityGraph, or eager fetch for specific use case. |
| 2 | LazyInitializationException | Accessing lazy association outside transaction (e.g., in view layer after service returns). | LazyInitializationException: could not initialize proxy | Keep access inside @Transactional, use DTO projection, or Open Session in View (OSIV — not recommended in production). |
| 3 | Exposing Entities in REST APIs | Returning JPA @Entity directly from controller. | Lazy loading errors, infinite recursion in JSON, over-posting security risk. | Use DTOs — map entity to DTO (MapStruct, ModelMapper, or manual). Never expose entities. |
| 4 | Missing @Transactional | Forgetting @Transactional on service methods with multiple repository calls. | Only last operation saved, or no rollback on error. | Always annotate service methods with @Transactional (or class level). |
| 5 | Incorrect Cascade / OrphanRemoval | Wrong cascade or orphanRemoval settings leading to unexpected deletes or missing saves. | Child entities not saved/deleted when parent is. | Understand cascade types (ALL, PERSIST, MERGE, REMOVE). Use orphanRemoval = true carefully. |
| 6 | Using Entity in Multiple Layers | Passing entity from repository → service → controller → view. | Accidental modification, serialization issues. | Convert to DTO as early as possible (in service layer). |
| 7 | No Validation on Input (DTO) | Accepting @RequestBody without @Valid and validation annotations. | Invalid data saved to DB. | Use @Valid @RequestBody EmployeeDto dto + Bean Validation annotations (@NotBlank, @Positive). |
| 8 | Open Session in View (OSIV) Enabled | Default in Spring Boot Web — keeps session open till view rendering. | Performance hit, unexpected lazy loading in view. | Disable in application.properties: spring.jpa.open-in-view=false (recommended for APIs). |
| 9 | Wrong Fetch Type | Leaving @ManyToOne as EAGER (default) when not needed. | Unnecessary data loading, performance degradation. | Explicitly set fetch = FetchType.LAZY when possible. |
| 10 | Modifying Entities in Read Operations | Accidentally modifying entity in read-only method (no @Transactional(readOnly = true)). | Unnecessary dirty checking, performance cost. | Use readOnly = true for query methods. |
| 11 | Bi-directional Relationship Issues | Forgetting mappedBy in @OneToMany or infinite recursion in JSON. | StackOverflowError in toString/JSON, duplicate FK columns. | Always use mappedBy, add @JsonIgnore or DTOs. |
| 12 | Using save() for Bulk Operations | Calling save() in loop for thousands of entities. | Very slow (one query per entity). | Use saveAll() or batch inserts (spring.jpa.properties.hibernate.jdbc.batch_size). |
What are the best practices in Spring Boot JPA + CRUD ?
1. Never expose JPA Entities in REST APIs
- Use DTOs (Data Transfer Objects) for request/response.
- Reason: Avoid lazy loading exceptions, over-posting, infinite recursion in JSON.
2. Keep Controllers Thin
- Controllers: Only handle HTTP (mapping, @RequestBody, ResponseEntity).
- Move business logic to Service layer.
3. Place @Transactional in Service Layer
- Not in controller or repository.
- Use `@Transactional(readOnly = true)` for query methods.
4. Validate Input on DTOs
- Use `@Valid @RequestBody EmployeeDto dto` + Bean Validation (`@NotBlank`, `@Positive`, `@Email`).
- Handle errors globally with `@ControllerAdvice`.
5. Prefer LAZY Fetching
- Default for `@OneToMany` — good.
- Change `@ManyToOne` to `fetch = FetchType.LAZY` when possible to avoid unnecessary loads.
6. Use DTO Projections or EntityGraph for N+1 Problem
- Avoid accessing lazy collections in loops.
- Use `@EntityGraph`, fetch join in `@Query`, or DTO projections.
7. Disable Open Session in View (OSIV) for APIs
- `spring.jpa.open-in-view=false` in `application.properties`.
- Prevents lazy loading in view layer (better performance).
8. Use Proper Cascade and orphanRemoval
- `cascade = CascadeType.ALL` + `orphanRemoval = true` on `@OneToMany` when children belong only to parent.
- Be careful — wrong settings cause unexpected deletes.
9. Use PagingAndSortingRepository or Pageable for Lists
- Never return full `List<T>` for large tables — use `Page<T>` with `Pageable`.
10. Logging and Exception Handling
- Log at appropriate levels (info for entry/exit, error for exceptions).
- Global `@ControllerAdvice` for consistent error responses.
11. Use Constructor Expressions in Queries (for DTOs)
@Query("SELECT new com.example.dto.EmployeeDto(e.name, e.salary) FROM Employee e")
List<EmployeeDto> findEmployeeDtos();
12. Batch Inserts/Updates
- For bulk operations: `spring.jpa.properties.hibernate.jdbc.batch_size=50`.
1. Never expose JPA Entities in REST APIs
- Use DTOs (Data Transfer Objects) for request/response.
- Reason: Avoid lazy loading exceptions, over-posting, infinite recursion in JSON.
2. Keep Controllers Thin
- Controllers: Only handle HTTP (mapping, @RequestBody, ResponseEntity).
- Move business logic to Service layer.
3. Place @Transactional in Service Layer
- Not in controller or repository.
- Use `@Transactional(readOnly = true)` for query methods.
4. Validate Input on DTOs
- Use `@Valid @RequestBody EmployeeDto dto` + Bean Validation (`@NotBlank`, `@Positive`, `@Email`).
- Handle errors globally with `@ControllerAdvice`.
5. Prefer LAZY Fetching
- Default for `@OneToMany` — good.
- Change `@ManyToOne` to `fetch = FetchType.LAZY` when possible to avoid unnecessary loads.
6. Use DTO Projections or EntityGraph for N+1 Problem
- Avoid accessing lazy collections in loops.
- Use `@EntityGraph`, fetch join in `@Query`, or DTO projections.
7. Disable Open Session in View (OSIV) for APIs
- `spring.jpa.open-in-view=false` in `application.properties`.
- Prevents lazy loading in view layer (better performance).
8. Use Proper Cascade and orphanRemoval
- `cascade = CascadeType.ALL` + `orphanRemoval = true` on `@OneToMany` when children belong only to parent.
- Be careful — wrong settings cause unexpected deletes.
9. Use PagingAndSortingRepository or Pageable for Lists
- Never return full `List<T>` for large tables — use `Page<T>` with `Pageable`.
10. Logging and Exception Handling
- Log at appropriate levels (info for entry/exit, error for exceptions).
- Global `@ControllerAdvice` for consistent error responses.
11. Use Constructor Expressions in Queries (for DTOs)
@Query("SELECT new com.example.dto.EmployeeDto(e.name, e.salary) FROM Employee e")
List<EmployeeDto> findEmployeeDtos();
12. Batch Inserts/Updates
- For bulk operations: `spring.jpa.properties.hibernate.jdbc.batch_size=50`.
_____________________________________________________________________
Coding Practice :
=> Do the Setup for Spring Boot JPA + CRUD
=> Exercises (create Employee entity + repository + controller):
=> Exercises (create Employee entity + repository + controller):
- Create Employee entity (id, name, salary, department).
- Create EmployeeRepository extends JpaRepository.
- Test basic CRUD in main (save, findAll, findById).
- Custom method: findByName(String name).
- Custom method: findBySalaryGreaterThan(int salary).
- @Query JPQL: select by department.
- @Query native SQL.
- Pagination: Page<Employee> with Pageable.
- Sort by salary descending.
- EmployeeDto with validation (@NotBlank, @Positive).
- @PostMapping with @Valid + BindingResult.
- Custom error response for validation.
- @ControllerAdvice for MethodArgumentNotValidException.
- Delete endpoint with proper status.
- Update (PUT) endpoint.
- Mix: Department entity, @OneToMany relationship, custom queries, etc.
- Mix: Department entity, @OneToMany relationship, custom queries, etc.
- Mix: Department entity, @OneToMany relationship, custom queries, etc.
- Mix: Department entity, @OneToMany relationship, custom queries, etc.
- Mix: Department entity, @OneToMany relationship, custom queries, etc.