Spring Data JPA

SkillDatabases & data

Spring Data JPA for database access in Spring Boot applications. Covers repositories, entities, relationships, queries, pagination, and auditing. Based on production patterns from castellino and gestionale-presenze projects.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Spring Data JPA skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/backend-frameworks/spring-data-jpa/SKILL.md and read by ahel’s review.

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

AnnotationPurpose
@EntityJPA entity
@TableTable mapping
@IdPrimary key
@GeneratedValueAuto-generation strategy
@ColumnColumn mapping
@ManyToOne / @OneToManyRelationships
@QueryCustom JPQL/SQL
@ModifyingUpdate/Delete queries
@CreatedDate / @LastModifiedDateAuditing

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-PatternWhy It's BadCorrect Approach
N+1 queriesPoor performanceUse @EntityGraph or fetch joins
Missing @TransactionalData inconsistencyAlways use for write operations
Bidirectional relations without careInfinite recursionUse @JsonManagedReference/@JsonBackReference
Fetch EAGER everywhereLoads unnecessary dataUse LAZY, fetch only when needed
No paginationMemory issuesAlways paginate large results
Query in loopPerformance killerUse batch fetch or single query

Quick Troubleshooting

ProblemLikely CauseSolution
LazyInitializationExceptionAccessing lazy field outside transactionFetch in transaction or use @EntityGraph
MultipleBagFetchExceptionMultiple @OneToMany EAGER fetchUse @EntityGraph or separate queries
Slow queriesMissing indexes or N+1Add indexes, check query logs
No query resultsWrong method nameFollow naming convention or use @Query
Constraint violationEntity state mismatchCheck cascade and orphanRemoval
DetachedEntityExceptionEntity not managedUse merge() or reload entity

Reference Documentation

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
spring-data-jpa-claude-dev-suite
Source
github.com/claude-dev-suite/claude-dev-suite