返回 Skill 列表
extension
分类: 开发与工程无需 API Key

spring-boot-jpa-repository

实现Spring Data JPA仓库的指南,包括查询、事务和数据访问模式的最佳实践。在创建或修改数据访问层代码时使用本指南。

person作者: jakexiaohubgithub

Spring Data JPA Repository Best Practices

Follow these practices for efficient and maintainable data access.

Repository Interface

@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

@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

@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

// 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

@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

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

@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

@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

@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

@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