Skip to main content

Spring Data JPA Repositories and Query Patterns

This guide focuses on day-to-day Spring Data JPA usage: repository design, query styles, pagination, and composable search patterns.

What Is Spring Data JPA?โ€‹

Spring Data JPA is part of the larger Spring Data project. It provides a repository abstraction on top of JPA that eliminates the need to write boilerplate data access code.

Important Distinction: Spring Data JPA is not a JPA provider.

  • JPA is the specification for object-relational mapping in Java.
  • Hibernate is the default JPA implementation (provider) that handles the actual database operations.
  • Spring Data JPA sits on top of the JPA provider. It hides the EntityManager complexity and provides a clean interface for data access.

Instead of manually writing DAO (Data Access Object) implementations, you define interfaces. At runtime, Spring creates a proxy implementation of these interfaces automatically.

Why Use Spring Data JPA?โ€‹

Problems It Solvesโ€‹

ProblemHow Spring Data JPA Fixes It
Repetitive CRUD boilerplateAuto-generated proxy implementations at runtime
Manual query writing for simple operationsQuery derivation from method names
Complex pagination and sorting logicBuilt-in Pageable and Sort support
Tedious EntityManager managementAutomatic session and transaction handling
Verbose DAO layerSingle interface replaces the DAO class
Migration workflow consistencyWorks with Flyway/Liquibase

Core Benefitsโ€‹

  1. Zero boilerplate for CRUD and common list operations.
  2. Derived queries from method names.
  3. Custom JPQL/native queries via annotations.
  4. First-class pagination and sorting support.
  5. Auditing integration.
  6. Tight Spring Boot integration.

Defining Entitiesโ€‹

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

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

@Column(nullable = false, unique = true, length = 50)
private String username;

@Column(nullable = false)
private String email;

@Enumerated(EnumType.STRING)
private UserStatus status;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;
}

Key Annotationsโ€‹

AnnotationPurpose
@EntityMarks class as JPA entity
@TableDefines DB table mapping
@IdPrimary key field
@GeneratedValueKey generation strategy
@ColumnColumn constraints and options
@EnumeratedEnum persistence strategy
@TemporalDate/Calendar precision mapping
@CreatedDate / @LastModifiedDateAuditing timestamps
@OneToMany / @ManyToOne / @ManyToManyRelationship mapping

Repository Hierarchyโ€‹

Spring Data JPA provides a specific hierarchy of interfaces, each adding more specialized functionality:

Repository (Marker interface, no methods)
-> CrudRepository (Adds basic CRUD operations like save, findById, delete)
-> PagingAndSortingRepository (Adds findAll(Pageable) and findAll(Sort))
-> JpaRepository (Adds JPA-specific methods like flush(), batch deletes)

Use JpaRepository in most projects for richer APIs (e.g., managing the persistence context via flush, batch operations, and pagination integration).

Setup and Creating Repositoriesโ€‹

Dependencies & Configurationโ€‹

In a Spring Boot application, you only need to include the starter dependency:

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

Spring Boot automatically configures the database connection based on your application.properties (or application.yml) and scans for interfaces extending a Spring Data repository.

Defining the Interfaceโ€‹

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

By just declaring this interface, Spring injects a proxy implementation into your application context. You instantly gain access to methods like:

  • save, saveAll
  • findById, findAll, count, existsById
  • deleteById, deleteAll
  • findAll(Pageable), findAll(Sort)

Query Derivationโ€‹

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);

List<User> findByStatusAndEmailContaining(UserStatus status, String email);

List<User> findByAgeGreaterThanOrderByLastNameAsc(int age);

long countByStatus(UserStatus status);

void deleteByStatus(UserStatus status);
}

Useful Naming Keywordsโ€‹

KeywordExampleSQL Fragment
AndfindByFirstNameAndLastNameWHERE first_name = ? AND last_name = ?
OrfindByFirstNameOrLastNameWHERE first_name = ? OR last_name = ?
BetweenfindByAgeBetweenWHERE age BETWEEN ? AND ?
LessThan / GreaterThanfindByAgeLessThanWHERE age < ?
Like / ContainingfindByNameContainingWHERE name LIKE %?%
InfindByStatusInWHERE status IN (?)
OrderByfindByOrderByNameAscORDER BY name ASC
IsNull / IsNotNullfindByEmailIsNullWHERE email IS NULL
True / FalsefindByActiveTrueWHERE active = true
Top / FirstfindTop5ByOrderByCreatedAtDescLIMIT 5

Custom Queriesโ€‹

JPQL with @Queryโ€‹

@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.createdAt DESC")
List<User> findActiveUsers(@Param("status") UserStatus status);

Native SQLโ€‹

@Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
Optional<User> findByEmailNative(@Param("email") String email);

Update/Delete Queriesโ€‹

@Modifying
@Transactional
@Query("UPDATE User u SET u.status = :status WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("status") UserStatus status,
@Param("date") LocalDateTime date);

Pagination and Sortingโ€‹

Page<User> findByStatus(UserStatus status, Pageable pageable);
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> page = userRepository.findByStatus(UserStatus.ACTIVE, pageable);

Relationships and Fetchingโ€‹

@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}

Many-to-Manyโ€‹

@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}

Fetch Typesโ€‹

FetchTypeBehaviorDefault For
EAGERLoads related entities immediately with parent@ManyToOne, @OneToOne
LAZYLoads related entities only when accessed@OneToMany, @ManyToMany

Avoiding Bidirectional Serialization Issuesโ€‹

@JsonManagedReference
private List<Order> orders;

@JsonBackReference
private User user;

Or better, use DTOs for API contracts.

Composite Primary Keysโ€‹

@Embeddable
public class OrderItemId implements Serializable {
private Long orderId;
private Long productId;
}

@Entity
public class OrderItem {
@EmbeddedId
private OrderItemId id;

private int quantity;
private BigDecimal price;
}

Best practice:

  • Default to LAZY.
  • Fetch explicitly with JOIN FETCH or @EntityGraph when required.
  • Prefer DTOs for API responses to avoid recursive serialization issues.

Query By Example (QBE)โ€‹

User probe = new User();
probe.setStatus(UserStatus.ACTIVE);

ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("id", "createdAt");

Example<User> example = Example.of(probe, matcher);
List<User> users = userRepository.findAll(example);
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}

Use Specifications when filter fields are optional and combinable.

Projectionsโ€‹

Interface Projectionโ€‹

public interface UserSummary {
String getUsername();
String getEmail();

@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}

DTO Projectionโ€‹

@Query("SELECT new com.example.dto.UserDto(u.username, u.email) FROM User u WHERE u.status = :status")
List<UserDto> findUserDtosByStatus(@Param("status") UserStatus status);

Dynamic Projectionโ€‹

<T> List<T> findByStatus(UserStatus status, Class<T> type);

Projection Comparisonโ€‹

TypePerformanceJoin supportSpELUse case
Interface (closed)BestNoYesSimple field subsets
Class (DTO)GoodYes via JPQLNoAggregated response shapes
Open interfaceModerateNoYesComputed fields
DynamicVariesDependsDependsFlexible APIs

Common CrudRepository Methodsโ€‹

MethodDescription
save(entity)Insert or update depending on identifier state
saveAll(entities)Save a collection
findById(id)Returns Optional<T>
existsById(id)Returns existence boolean
findAll()Returns all rows
count()Returns total row count
deleteById(id)Delete by identifier
delete(entity)Delete one entity
deleteAll()Delete all rows

findById() vs getReferenceById()โ€‹

MethodBehavior
findById()Immediate fetch with Optional
getReferenceById()Lazy proxy, may throw on access if missing

delete() vs deleteInBatch()โ€‹

delete() triggers lifecycle callbacks and cascades. deleteInBatch() is efficient for bulk delete but skips lifecycle callbacks.

Auditingโ€‹

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig { }
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;
}

Compare Nextโ€‹