Spring Data JPA¶
Spring Data JPA simplifies database access by providing repository abstractions over JPA/Hibernate. Extend JpaRepository<Entity, ID> to get CRUD + pagination out of the box. Supports derived query methods (findByName), @Query (JPQL/native), and Specifications for dynamic queries. Key concepts: entity mapping, relationships, lazy vs eager loading, N+1 problem, @Transactional.
Repository (marker)
└── CrudRepository (CRUD operations)
└── ListCrudRepository
└── PagingAndSortingRepository (pagination + sort)
└── JpaRepository (JPA-specific: flush, batch)
Entity Mapping¶
@Entity marks a JPA entity mapped to a database table. Key annotations: @Id + @GeneratedValue (primary key strategy), @Column (column constraints), @Enumerated(EnumType.STRING) (enum mapping), @CreationTimestamp/@UpdateTimestamp (auto timestamps), @OneToMany, @ManyToOne, @ManyToMany (relationships).
Deep Dive: Entity Example
@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 UserStatus status;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
}
| ID Strategy | Description |
|---|---|
IDENTITY |
Database auto-increment |
SEQUENCE |
Database sequence (preferred for batch) |
TABLE |
Separate table for IDs |
AUTO |
Provider picks |
Relationship Mappings¶
@OneToMany / @ManyToOne — owner side has @JoinColumn, inverse side has mappedBy. @ManyToMany — uses a @JoinTable. @OneToOne — @JoinColumn on owner. Always use FetchType.LAZY on @ManyToOne/@OneToOne for performance. Cascade types: ALL, PERSIST, MERGE, REMOVE.
Deep Dive: Relationship Examples
// OneToMany / ManyToOne
@Entity
public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders;
}
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
// ManyToMany
@Entity
public class Student {
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private Set<Course> courses;
}
Cascade types:
- PERSIST — save parent → save child
- MERGE — update parent → update child
- REMOVE — delete parent → delete child
- ALL — all of the above + DETACH, REFRESH
Derived Query Methods¶
Spring Data auto-generates queries from method names. Keywords: findBy, countBy, deleteBy, existsBy + property names + conditions (And, Or, Between, GreaterThan, Containing, In, OrderBy). No implementation needed — Spring generates the SQL.
Deep Dive: Example Methods
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String name);
List<User> findByStatusAndAgeGreaterThan(UserStatus status, int age);
List<User> findByNameIn(Collection<String> names);
List<User> findAllByOrderByCreatedAtDesc();
long countByStatus(UserStatus status);
boolean existsByEmail(String email);
void deleteByStatus(UserStatus status);
}
@Query (Custom Queries)¶
For complex queries, use @Query with JPQL (entity-based) or native SQL. Use @Param for named parameters. For updates, add @Modifying. Projections return only specific columns via interfaces. JPQL is preferred (portable); native SQL for database-specific features.
Deep Dive: JPQL, Native, and Projections
// JPQL
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findByEmailAndStatus(@Param("email") String email,
@Param("status") UserStatus status);
// Native SQL
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
Optional<User> findByEmailNative(String email);
// Update — requires @Modifying
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
// Projection
@Query("SELECT u.name as name, u.email as email FROM User u")
List<UserProjection> findAllProjected();
public interface UserProjection {
String getName();
String getEmail();
}
Lazy vs Eager Loading & N+1 Problem¶
Defaults: @ManyToOne/@OneToOne are EAGER, @OneToMany/@ManyToMany are LAZY. The N+1 problem: loading N entities triggers N additional queries for their lazy associations. Fix with: JOIN FETCH (JPQL), @EntityGraph (declarative), or batch fetching (hibernate.default_batch_fetch_size). Best practice: default to LAZY, fetch eagerly only when needed.
Deep Dive: Solutions
// Problem: 1 query for users + N queries for orders
List<User> users = userRepository.findAll();
users.forEach(u -> u.getOrders().size()); // Triggers N queries!
// Fix 1: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.status = :status")
List<User> findByStatusWithOrders(@Param("status") UserStatus status);
// Fix 2: @EntityGraph
@EntityGraph(attributePaths = {"orders", "roles"})
List<User> findAll();
// Fix 3: Batch fetching (application.yml)
// spring.jpa.properties.hibernate.default_batch_fetch_size=20
Pagination & Sorting¶
JpaRepository supports pagination via Pageable parameter in repository methods. Create with PageRequest.of(page, size, Sort.by(...)). Returns Page<T> with content, total count, page info. Use in controllers with @RequestParam for page/size/sort.
Deep Dive: Example
// Repository
Page<User> findByStatus(UserStatus status, Pageable pageable);
// Service
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> page = userRepository.findByStatus(UserStatus.ACTIVE, pageable);
page.getContent(); // List<User>
page.getTotalElements(); // Total count
page.getTotalPages(); // Total pages
// Controller
@GetMapping("/users")
public Page<UserResponse> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userRepository.findAll(PageRequest.of(page, size)).map(this::toResponse);
}
@Transactional¶
@Transactional wraps a method in a database transaction — if any step fails, everything rolls back. Default propagation: REQUIRED (join existing or create new). Use readOnly = true for read queries (optimization). Pitfall: self-invocation bypasses the proxy — @Transactional is ignored when calling from within the same class.
Deep Dive: Propagation & Self-Invocation Pitfall
| Type | Behavior |
|---|---|
REQUIRED (default) |
Join existing or create new |
REQUIRES_NEW |
Always create new, suspend existing |
MANDATORY |
Must have existing, throw if not |
NESTED |
Nested (savepoint) within existing |
Self-invocation pitfall:
@Service
public class UserService {
public void method1() {
method2(); // @Transactional IGNORED — no proxy!
}
@Transactional
public void method2() { ... }
}
@Transactional works via AOP proxies. Self-calls bypass the proxy. Fix: extract to a separate bean.
Common Interview Questions¶
Common Interview Questions
- What is Spring Data JPA? How is it different from JPA/Hibernate?
- What is the difference between
CrudRepositoryandJpaRepository? - How do derived query methods work?
- What is the N+1 problem and how do you fix it?
- What is the difference between lazy and eager fetching?
- How does
@Transactionalwork? What is the default propagation? - Why doesn't
@Transactionalwork on self-invocation? - How do you implement pagination?
- What is
@EntityGraph? - What are cascade types? When do you use
orphanRemoval?