Spring Data JPA
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: spring-data-jpa for comprehensive documentation.
Entity with Auditing
@Entity @Table(name = "users") @Data @NoArgsConstructor @AllArgsConstructor @Builder @EntityListeners(AuditingEntityListener.class) 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)
@Column(nullable = false)
private UserRole role = UserRole.USER;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
Repository Interface
@Repository public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByStatus(UserStatus status);
List<User> findByRoleIn(List<UserRole> roles);
// Query with JPQL
@Query("SELECT u FROM User u WHERE u.status = :status AND u.role = :role")
List<User> findByStatusAndRole(
@Param("status") UserStatus status,
@Param("role") UserRole role
);
// Native query
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List<User> findByEmailDomain(@Param("domain") String domain);
// Pagination
Page<User> findByNameContainingIgnoreCase(String name, Pageable pageable);
// Sorting
List<User> findByStatus(UserStatus status, Sort sort);
// Modifying queries
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteByStatus(@Param("status") UserStatus status);
}
Relationships
// One-to-Many @Entity public class Department { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
}
@Entity public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
}
// Many-to-Many @Entity public class User { @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set<Role> roles = new HashSet<>(); }
Pagination & Sorting
@Service public class UserService {
public Page<UserResponse> findAll(int page, int size, String sortBy, String direction) {
Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return userRepository.findAll(pageable)
.map(userMapper::toResponse);
}
public Page<UserResponse> search(String query, Pageable pageable) {
return userRepository.findByNameContainingIgnoreCase(query, pageable)
.map(userMapper::toResponse);
}
}
// Controller @GetMapping public ResponseEntity<Page<UserResponse>> findAll( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "createdAt") String sortBy, @RequestParam(defaultValue = "desc") String direction) { return ResponseEntity.ok(userService.findAll(page, size, sortBy, direction)); }
Specifications (Dynamic Queries)
public class UserSpecifications {
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<User> hasRole(UserRole role) {
return (root, query, cb) ->
role == null ? null : cb.equal(root.get("role"), role);
}
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")),
"%" + name.toLowerCase() + "%");
}
}
// Repository extends JpaSpecificationExecutor public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}
// Usage Specification<User> spec = Specification .where(UserSpecifications.hasStatus(UserStatus.ACTIVE)) .and(UserSpecifications.hasRole(UserRole.ADMIN)) .and(UserSpecifications.nameContains("john"));
List<User> users = userRepository.findAll(spec);
Enable Auditing
@Configuration @EnableJpaAuditing public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
Key Annotations
Annotation Purpose
@Entity
JPA entity
@Table
Table mapping
@Id
Primary key
@GeneratedValue
Auto-generation strategy
@Column
Column mapping
@ManyToOne / @OneToMany
Relationships
@Query
Custom JPQL/SQL
@Modifying
Update/Delete queries
@CreatedDate / @LastModifiedDate
Auditing
When NOT to Use This Skill
-
Spring Boot application setup → Use spring-boot skill
-
REST API patterns → Use spring-web skill
-
MongoDB operations → Use mongodb-expert skill
-
Security configuration → Use spring-security skill
-
Raw SQL optimization → Use sql-expert skill
-
Reactive database access → Use spring-r2dbc skill
Anti-Patterns
Anti-Pattern Why It's Bad Correct Approach
N+1 queries Poor performance Use @EntityGraph or fetch joins
Missing @Transactional
Data inconsistency Always use for write operations
Bidirectional relations without care Infinite recursion Use @JsonManagedReference/@JsonBackReference
Fetch EAGER everywhere Loads unnecessary data Use LAZY, fetch only when needed
No pagination Memory issues Always paginate large results
Query in loop Performance killer Use batch fetch or single query
Quick Troubleshooting
Problem Likely Cause Solution
LazyInitializationException Accessing lazy field outside transaction Fetch in transaction or use @EntityGraph
MultipleBagFetchException Multiple @OneToMany EAGER fetch Use @EntityGraph or separate queries
Slow queries Missing indexes or N+1 Add indexes, check query logs
No query results Wrong method name Follow naming convention or use @Query
Constraint violation Entity state mismatch Check cascade and orphanRemoval
DetachedEntityException Entity not managed Use merge() or reload entity
Reference Documentation
-
Spring Data JPA Reference
-
Query Methods
-
Specifications