Spring Data JPA

Simplifying database access with Spring Data

← Back to Index

What is Spring Data JPA?

Spring Data JPA eliminates boilerplate code:

Spring Data JPA is a layer on top of JPA (Java Persistence API) that provides:

  • Repository interfaces with auto-generated implementations
  • Query derivation from method names
  • Custom JPQL and native SQL queries
  • Pagination and sorting support
  • Auditing capabilities

Setup

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Database driver (e.g., H2 for development) -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- Or PostgreSQL for production -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Entity Classes

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

@Entity
@Table(name = "users")
public class User {

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

    @Column(nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)
    private Status status = Status.ACTIVE;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();

    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    // Constructors, getters, setters
    public User() {}

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // getters/setters...
}

public enum Status {
    ACTIVE, INACTIVE, SUSPENDED
}

Repository Interfaces

Basic Repository

import org.springframework.data.jpa.repository.JpaRepository;

// That's it! Spring Data provides the implementation
public interface UserRepository extends JpaRepository<User, Long> {
    // Inherited methods:
    // save(entity), saveAll(entities)
    // findById(id), findAll(), findAllById(ids)
    // count(), existsById(id)
    // deleteById(id), delete(entity), deleteAll()
    // findAll(Pageable), findAll(Sort)
}

Query Methods (Derived Queries)

public interface UserRepository extends JpaRepository<User, Long> {

    // Spring Data generates queries from method names!

    // SELECT * FROM users WHERE email = ?
    Optional<User> findByEmail(String email);

    // SELECT * FROM users WHERE name = ?
    List<User> findByName(String name);

    // SELECT * FROM users WHERE status = ?
    List<User> findByStatus(Status status);

    // SELECT * FROM users WHERE name LIKE '%value%'
    List<User> findByNameContaining(String name);

    // SELECT * FROM users WHERE name LIKE 'value%'
    List<User> findByNameStartingWith(String prefix);

    // SELECT * FROM users WHERE status = ? AND name = ?
    List<User> findByStatusAndName(Status status, String name);

    // SELECT * FROM users WHERE status = ? OR name = ?
    List<User> findByStatusOrName(Status status, String name);

    // SELECT * FROM users WHERE created_at BETWEEN ? AND ?
    List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);

    // SELECT * FROM users WHERE status IN (?, ?, ?)
    List<User> findByStatusIn(Collection<Status> statuses);

    // SELECT * FROM users WHERE email IS NOT NULL
    List<User> findByEmailIsNotNull();

    // SELECT * FROM users ORDER BY name ASC
    List<User> findByStatusOrderByNameAsc(Status status);

    // SELECT * FROM users LIMIT 1
    Optional<User> findFirstByStatus(Status status);

    // SELECT * FROM users LIMIT 5
    List<User> findTop5ByStatus(Status status);

    // SELECT COUNT(*) FROM users WHERE status = ?
    long countByStatus(Status status);

    // SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)
    boolean existsByEmail(String email);

    // DELETE FROM users WHERE status = ?
    void deleteByStatus(Status status);
}

Custom JPQL Queries

public interface UserRepository extends JpaRepository<User, Long> {

    // JPQL query
    @Query("SELECT u FROM User u WHERE u.email = :email")
    Optional<User> findByEmailAddress(@Param("email") String email);

    // JPQL with JOIN
    @Query("SELECT u FROM User u JOIN u.roles r WHERE r.name = :roleName")
    List<User> findByRoleName(@Param("roleName") String roleName);

    // JPQL with FETCH (eager loading)
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);

    // Native SQL query
    @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
    Optional<User> findByEmailNative(String email);

    // Projection (return specific fields)
    @Query("SELECT u.name as name, u.email as email FROM User u")
    List<UserSummary> findAllSummaries();

    // Update query
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") Status status);

    // Delete query
    @Modifying
    @Query("DELETE FROM User u WHERE u.status = :status")
    int deleteByStatusJpql(@Param("status") Status status);
}

// Projection interface
public interface UserSummary {
    String getName();
    String getEmail();
}

Pagination and Sorting

public interface UserRepository extends JpaRepository<User, Long> {

    // Pageable support
    Page<User> findByStatus(Status status, Pageable pageable);

    // Sort support
    List<User> findByStatus(Status status, Sort sort);

    // Slice (like Page but without total count)
    Slice<User> findByNameContaining(String name, Pageable pageable);
}

// Service using pagination
@Service
public class UserService {

    private final UserRepository userRepository;

    public Page<User> getUsers(int page, int size) {
        // Page 0, size 10
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findAll(pageable);
    }

    public Page<User> getUsersSorted(int page, int size) {
        // Sort by name ascending, then by createdAt descending
        Pageable pageable = PageRequest.of(page, size,
            Sort.by("name").ascending()
                 .and(Sort.by("createdAt").descending())
        );
        return userRepository.findAll(pageable);
    }

    public List<User> getActiveUsersSorted() {
        // Just sorting, no pagination
        return userRepository.findByStatus(
            Status.ACTIVE,
            Sort.by(Sort.Direction.DESC, "createdAt")
        );
    }
}

// Controller with pagination
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public Page<User> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {

        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        return userService.findAll(pageable);
    }
}

// Page response example:
// {
//   "content": [...users...],
//   "totalElements": 100,
//   "totalPages": 10,
//   "number": 0,
//   "size": 10,
//   "first": true,
//   "last": false
// }

Specifications (Dynamic Queries)

public interface UserRepository extends JpaRepository<User, Long>,
                                       JpaSpecificationExecutor<User> {
}

// Specification builder
public class UserSpecifications {

    public static Specification<User> hasStatus(Status status) {
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }

    public static Specification<User> nameLike(String name) {
        return (root, query, cb) ->
            cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
    }

    public static Specification<User> createdAfter(LocalDateTime date) {
        return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
    }

    public static Specification<User> hasRole(String roleName) {
        return (root, query, cb) -> {
            Join<User, Role> roles = root.join("roles");
            return cb.equal(roles.get("name"), roleName);
        };
    }
}

// Using specifications
@Service
public class UserSearchService {

    private final UserRepository userRepository;

    public List<User> search(UserSearchCriteria criteria) {
        Specification<User> spec = Specification.where(null);

        if (criteria.getStatus() != null) {
            spec = spec.and(UserSpecifications.hasStatus(criteria.getStatus()));
        }
        if (criteria.getName() != null) {
            spec = spec.and(UserSpecifications.nameLike(criteria.getName()));
        }
        if (criteria.getCreatedAfter() != null) {
            spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
        }

        return userRepository.findAll(spec);
    }
}

Auditing

// Enable auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}

// Base auditable entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;

    // getters/setters
}

// Entity extending Auditable
@Entity
public class User extends Auditable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields...
}

// Provide current user for @CreatedBy/@LastModifiedBy
@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // Get from Spring Security context
        return Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
}

Best Practices

DO:

  • Use Optional for single results - findById returns Optional
  • Use pagination for large datasets - Page or Slice
  • Use projections for partial data - Better performance
  • Use @Transactional on service layer - Not repository
  • Use fetch joins for N+1 queries - LEFT JOIN FETCH
  • Use specifications for dynamic queries - Composable

DON'T:

  • Don't return entities to controllers - Use DTOs
  • Don't use findAll() without limits - Use pagination
  • Don't forget @Modifying - Required for UPDATE/DELETE
  • Don't ignore lazy loading - Use eager fetch when needed
  • Don't use native queries if JPQL works - Less portable

Summary

  • Spring Data JPA: Simplifies JPA data access
  • JpaRepository: CRUD + pagination + sorting
  • Query Methods: findByX, countByX, deleteByX
  • @Query: Custom JPQL or native SQL
  • Pageable: Pagination with PageRequest.of()
  • Sort: Sorting with Sort.by()
  • Specifications: Dynamic, composable queries
  • Auditing: @CreatedDate, @LastModifiedDate
  • Projections: Return specific fields only