Spring Data Jpa
Why Spring Data JPA Matters
Spring Data JPA eliminates manual EntityManager code by generating repository implementations from interfaces. In production systems with 50+ domain entities, manual JPQL queries require 1000+ lines of boilerplate DAO code—Spring Data JPA reduces this to interface declarations with method name query derivation, enabling 90% less code while maintaining type safety.
Problem: Manual JPA requires EntityManager injection and JPQL for every database operation.
Solution: Spring Data JPA auto-generates repository implementations from interfaces with query derivation.
Manual JPA Repository
@Repository
public class ZakatDonationRepository {
@PersistenceContext
private EntityManager entityManager; // => Manual injection
public Optional<ZakatDonation> findById(Long id) {
// => Manual JPQL query
ZakatDonation donation = entityManager.find(ZakatDonation.class, id);
return Optional.ofNullable(donation);
}
public List<ZakatDonation> findByDonorName(String name) {
// => Type-unsafe string query
return entityManager
.createQuery("SELECT d FROM ZakatDonation d WHERE d.donorName = :name",
ZakatDonation.class)
.setParameter("name", name)
.getResultList();
}
public ZakatDonation save(ZakatDonation donation) {
if (donation.getId() == null) {
entityManager.persist(donation); // => Insert
return donation;
} else {
return entityManager.merge(donation); // => Update
}
}
}Limitations: Boilerplate for CRUD, type-unsafe queries, manual persist/merge logic.
Spring Data JPA Repository
@Repository
public interface ZakatDonationRepository extends JpaRepository<ZakatDonation, Long> {
// => Spring Data generates implementation automatically
// => Inherits: findById, findAll, save, delete, count
List<ZakatDonation> findByDonorName(String donorName);
// => Method name parsed: findBy + DonorName → WHERE donorName = ?
// => Spring Data generates JPQL automatically
@Query("SELECT d FROM ZakatDonation d WHERE d.amount >= :minAmount")
// => Custom JPQL query (type-checked at compile time with IDE support)
List<ZakatDonation> findLargeDonations(@Param("minAmount") BigDecimal minAmount);
@Query(value = "SELECT * FROM zakat_donations WHERE YEAR(created_at) = ?1",
nativeQuery = true)
// => Native SQL when JPQL insufficient
List<ZakatDonation> findByYear(int year);
}What Spring Data JPA provides:
- Auto-generated CRUD methods (save, findById, findAll, delete)
- Query derivation from method names (findBy*, countBy*, existsBy*)
- Pagination and sorting (Page
, Sort) - Custom queries via @Query annotation
- Specification API for dynamic queries
- Auditing (@CreatedDate, @LastModifiedDate)
Query Derivation Examples
public interface DonationRepository extends JpaRepository<ZakatDonation, Long> {
// => WHERE donorName = ? AND amount > ?
List<ZakatDonation> findByDonorNameAndAmountGreaterThan(
String name, BigDecimal amount);
// => WHERE createdAt BETWEEN ? AND ?
List<ZakatDonation> findByCreatedAtBetween(LocalDate start, LocalDate end);
// => WHERE status IN (?, ?, ?)
List<ZakatDonation> findByStatusIn(List<DonationStatus> statuses);
// => ORDER BY amount DESC
List<ZakatDonation> findTop10ByOrderByAmountDesc();
// => EXISTS (SELECT 1 FROM ...)
boolean existsByDonorEmailAndStatus(String email, DonationStatus status);
// => COUNT(*) WHERE ...
long countByStatus(DonationStatus status);
}Pagination and Sorting
@RestController
@RequestMapping("/api/donations")
public class DonationController {
@GetMapping
public Page<DonationResponse> getDonations(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt,desc") String[] sort) {
// => Parse sort parameter: "createdAt,desc" → Sort.by("createdAt").descending()
Sort sortObj = Sort.by(
sort[1].equals("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
sort[0]
);
// => Pageable: page number, size, sort
Pageable pageable = PageRequest.of(page, size, sortObj);
// => Spring Data executes: SELECT ... LIMIT ? OFFSET ?
Page<ZakatDonation> donations = repository.findAll(pageable);
// => Return: content, totalElements, totalPages, number, size
return donations.map(this::toResponse);
}
}Response structure:
{
"content": [{ "id": 1, "amount": 1000 }],
"pageable": { "pageNumber": 0, "pageSize": 20 },
"totalElements": 150,
"totalPages": 8,
"last": false,
"first": true
}Production Configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/zakat_db
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20 # => Connection pool
jpa:
hibernate:
ddl-auto: validate # => Production: validate only (no auto-DDL)
properties:
hibernate:
jdbc:
batch_size: 20 # => Batch inserts (performance)
order_inserts: true # => Order for batching
order_updates: true
open-in-view: false # => Disable OSIV (anti-pattern in production)Trade-offs: Spring Data covers 95% queries. Native SQL + JDBC for complex reports (performance-critical).
Next Steps
- Transaction Management - @Transactional patterns
- Database Initialization - Flyway/Liquibase integration
- Multiple Datasources - Multi-database configuration