Agent skill

spring-boot-jpa-repository

Guide for implementing Spring Data JPA repositories with best practices for queries, transactions, and data access patterns. Use this when creating or modifying data access layer code.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/spring-boot-jpa-repository

SKILL.md

Spring Data JPA Repository Best Practices

Follow these practices for efficient and maintainable data access.

Repository Interface

java
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    
    // Query derivation - Spring generates the query
    Optional<Customer> findByEmail(String email);
    
    List<Customer> findByGuestFalse();
    
    List<Customer> findByNameContainingIgnoreCase(String name);
    
    boolean existsByEmail(String email);
    
    // Pagination support
    Page<Customer> findByGuestFalse(Pageable pageable);
}

Custom Queries with @Query

java
@Repository
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
    
    // JPQL query
    @Query("SELECT a FROM Appointment a WHERE a.customer.id = :customerId AND a.status = :status")
    List<Appointment> findByCustomerAndStatus(
        @Param("customerId") Long customerId, 
        @Param("status") String status
    );
    
    // Native query (use sparingly)
    @Query(value = "SELECT * FROM appointments WHERE appointment_time BETWEEN ?1 AND ?2", 
           nativeQuery = true)
    List<Appointment> findByDateRange(LocalDateTime start, LocalDateTime end);
    
    // Join fetch to avoid N+1
    @Query("SELECT a FROM Appointment a " +
           "LEFT JOIN FETCH a.customer " +
           "LEFT JOIN FETCH a.employee " +
           "WHERE a.id = :id")
    Optional<Appointment> findByIdWithDetails(@Param("id") Long id);
    
    // Aggregate queries
    @Query("SELECT COUNT(a) FROM Appointment a WHERE a.employee.id = :employeeId AND a.status = 'COMPLETED'")
    long countCompletedByEmployee(@Param("employeeId") Long employeeId);
}

Modifying Queries

java
@Repository
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
    
    @Modifying
    @Query("UPDATE Appointment a SET a.status = :status WHERE a.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") String status);
    
    @Modifying
    @Query("DELETE FROM Appointment a WHERE a.status = 'CANCELLED' AND a.appointmentTime < :before")
    int deleteCancelledBefore(@Param("before") LocalDateTime before);
}

Projections

java
// Interface projection - only selected fields
public interface CustomerSummary {
    Long getId();
    String getName();
    String getEmail();
}

// DTO projection - full control
public record CustomerDTO(Long id, String name, String email) {}

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    
    // Interface projection
    List<CustomerSummary> findAllProjectedBy();
    
    // DTO projection with JPQL
    @Query("SELECT new com.salonhub.api.customer.dto.CustomerDTO(c.id, c.name, c.email) FROM Customer c")
    List<CustomerDTO> findAllAsDTO();
    
    // Dynamic projection
    <T> List<T> findByGuestFalse(Class<T> type);
}

Entity Graphs

java
@Entity
@NamedEntityGraph(
    name = "Appointment.withDetails",
    attributeNodes = {
        @NamedAttributeNode("customer"),
        @NamedAttributeNode("employee"),
        @NamedAttributeNode("serviceType")
    }
)
public class Appointment {
    // fields
}

@Repository
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
    
    @EntityGraph("Appointment.withDetails")
    Optional<Appointment> findById(Long id);
    
    @EntityGraph(attributePaths = {"customer", "employee"})
    List<Appointment> findByStatus(String status);
}

Specifications for Dynamic Queries

java
public class AppointmentSpecifications {
    
    public static Specification<Appointment> hasStatus(String status) {
        return (root, query, cb) -> 
            status == null ? null : cb.equal(root.get("status"), status);
    }
    
    public static Specification<Appointment> hasCustomer(Long customerId) {
        return (root, query, cb) -> 
            customerId == null ? null : cb.equal(root.get("customer").get("id"), customerId);
    }
    
    public static Specification<Appointment> hasDateBetween(LocalDateTime start, LocalDateTime end) {
        return (root, query, cb) -> {
            if (start == null && end == null) return null;
            if (start == null) return cb.lessThanOrEqualTo(root.get("appointmentTime"), end);
            if (end == null) return cb.greaterThanOrEqualTo(root.get("appointmentTime"), start);
            return cb.between(root.get("appointmentTime"), start, end);
        };
    }
}

// Usage
@Service
public class AppointmentService {
    
    public List<Appointment> search(String status, Long customerId, LocalDateTime start, LocalDateTime end) {
        Specification<Appointment> spec = Specification
            .where(AppointmentSpecifications.hasStatus(status))
            .and(AppointmentSpecifications.hasCustomer(customerId))
            .and(AppointmentSpecifications.hasDateBetween(start, end));
        
        return appointmentRepository.findAll(spec);
    }
}

Transaction Management

java
@Service
@Transactional(readOnly = true)  // Default read-only
public class AppointmentService {
    
    private final AppointmentRepository appointmentRepository;
    private final QueueService queueService;
    
    // Read operation - uses class-level readOnly=true
    public AppointmentResponseDTO findById(Long id) {
        return appointmentRepository.findById(id)
            .map(appointmentMapper::toResponseDTO)
            .orElseThrow(() -> new ResourceNotFoundException("Appointment not found"));
    }
    
    // Write operation - override to allow writes
    @Transactional
    public AppointmentResponseDTO create(AppointmentRequestDTO request) {
        Appointment appointment = appointmentMapper.toEntity(request);
        appointment = appointmentRepository.save(appointment);
        return appointmentMapper.toResponseDTO(appointment);
    }
    
    // Complex transaction spanning multiple services
    @Transactional(rollbackFor = Exception.class)
    public void completeAppointment(Long id) {
        Appointment appointment = appointmentRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Appointment not found"));
        
        appointment.setStatus("COMPLETED");
        appointmentRepository.save(appointment);
        
        // If this fails, entire transaction rolls back
        queueService.removeFromQueue(appointment.getCustomer().getId());
    }
}

Optimistic Locking

java
@Entity
public class Appointment {
    @Id
    private Long id;
    
    @Version
    private Long version;
    
    // Other fields
}

// Handling optimistic lock exception
@Service
public class AppointmentService {
    
    @Transactional
    @Retryable(value = OptimisticLockException.class, maxAttempts = 3)
    public void updateAppointment(Long id, AppointmentUpdateDTO dto) {
        Appointment appointment = appointmentRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Appointment not found"));
        
        // Update fields
        appointmentMapper.updateEntity(dto, appointment);
        
        // Save will throw OptimisticLockException if version mismatch
        appointmentRepository.save(appointment);
    }
}

Auditing

java
@Configuration
@EnableJpaAuditing
public class JpaConfig {
    
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .map(Authentication::getName);
    }
}

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
    
    @LastModifiedBy
    private String updatedBy;
}

@Entity
public class Appointment extends BaseEntity {
    // Entity-specific fields
}

Query Hints for Performance

java
@Repository
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
    
    @QueryHints(value = {
        @QueryHint(name = "org.hibernate.fetchSize", value = "50"),
        @QueryHint(name = "org.hibernate.readOnly", value = "true"),
        @QueryHint(name = "org.hibernate.cacheable", value = "true")
    })
    @Query("SELECT a FROM Appointment a WHERE a.status = :status")
    List<Appointment> findByStatusOptimized(@Param("status") String status);
}

Repository Checklist

  • Extend JpaRepository for full CRUD support
  • Use query derivation for simple queries
  • Use @Query for complex JPQL queries
  • Add JOIN FETCH or @EntityGraph to prevent N+1
  • Use projections when full entities aren't needed
  • Implement Specification for dynamic queries
  • Configure transactions appropriately
  • Add @Version for optimistic locking where needed
  • Enable auditing for tracking changes

Didn't find tool you were looking for?

Be as detailed as possible for better results