diff --git a/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java b/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java new file mode 100644 index 00000000000..2297ef23f98 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java @@ -0,0 +1,17 @@ +package org.springframework.samples.petclinic.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("patientRecords"); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordController.java b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordController.java index 4873d5bfc19..23271429dbd 100644 --- a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordController.java +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordController.java @@ -20,8 +20,7 @@ import java.util.concurrent.ThreadLocalRandom; @RestController -@RequestMapping("/api/patient-records") -public class PatientRecordController implements InitializingBean { +@RequestMapping("/api/patient-records")public class PatientRecordController implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(PatientRecordController.class); @@ -46,9 +45,7 @@ public PatientRecordController(PatientRecordDataService dataService, @Override public void afterPropertiesSet() throws Exception { this.otelTracer = openTelemetry.getTracer("PatientRecordController"); - } - - @PostMapping("/populate-records") + }@PostMapping("/populate-records") public ResponseEntity populateData(@RequestParam(name = "count", defaultValue = "6000000") int count) { logger.info("Received request to populate {} patient records.", count); if (count <= 0) { @@ -64,29 +61,54 @@ public ResponseEntity populateData(@RequestParam(name = "count", default logger.error("Error during patient records population", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error during data population: " + e.getMessage()); } - } - - @GetMapping(value = "/query-records", produces = "application/json") - public List> getRecords( - @RequestParam(name = "weight", defaultValue = "5000") int patientWeight, - @RequestParam(name = "repetitions", defaultValue = "1") int repetitions) { + }@GetMapping(value = "/query-records", produces = "application/json") +@Cacheable(value = "patientRecords", key = "#patientWeight + '-' + #page + '-' + #size") +public ResponseEntity> getRecords( + @RequestParam(name = "weight", defaultValue = "5000") int patientWeight, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size) { + + logger.info("Querying patient records by weight: {} (page: {}, size: {})", + patientWeight, page, size); + + try { + // Validate pagination parameters + if (page < 0 || size <= 0 || size > 100) { + return ResponseEntity.badRequest() + .body(Map.of("error", "Invalid pagination parameters. Page must be >= 0 and size must be between 1 and 100.")); + } - logger.info("Querying patient records by weight: {} (repetitions: {})", patientWeight, repetitions); + // Calculate offset + int offset = page * size; + // Get total count + String countSql = "SELECT COUNT(*) FROM patient_records WHERE patient_weight = ?"; + int totalRecords = mysqlJdbcTemplate.queryForObject(countSql, Integer.class, patientWeight); + + // Get paginated records String sql = "SELECT id, treatment_type, patient_weight, visit_date, treatment_completed, medical_notes " + - "FROM patient_records WHERE patient_weight = ?"; + "FROM patient_records WHERE patient_weight = ? " + + "LIMIT ? OFFSET ?"; - List> lastResults = null; - for (int i = 0; i < repetitions; i++) { - lastResults = mysqlJdbcTemplate.queryForList(sql, patientWeight); - } + List> records = mysqlJdbcTemplate.queryForList(sql, patientWeight, size, offset); + + Map response = new HashMap<>(); + response.put("records", records); + response.put("currentPage", page); + response.put("totalItems", totalRecords); + response.put("totalPages", (int) Math.ceil((double) totalRecords / size)); + + logger.info("Query completed. Found {} records for weight: {} (page: {}/{})", + records.size(), patientWeight, page + 1, response.get("totalPages")); + + return ResponseEntity.ok(response); - logger.info("Query completed. Found {} records for weight: {}", - lastResults != null ? lastResults.size() : 0, patientWeight); - return lastResults; + } catch (Exception e) { + logger.error("Error querying patient records", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Error querying patient records: " + e.getMessage())); } - - @DeleteMapping("/cleanup-records") +}}@DeleteMapping("/cleanup-records") public ResponseEntity cleanupRecords() { logger.info("Received request to cleanup all patient records."); try { @@ -96,9 +118,7 @@ public ResponseEntity cleanupRecords() { logger.error("Error during patient records cleanup", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error during cleanup: " + e.getMessage()); } - } - - @PostMapping("/recreate-and-populate-records") + }@PostMapping("/recreate-and-populate-records") public ResponseEntity recreateAndPopulateRecords(@RequestParam(name = "count", defaultValue = "6000000") int count) { logger.info("Received request to recreate and populate {} patient records.", count); if (count <= 0) { @@ -110,9 +130,7 @@ public ResponseEntity recreateAndPopulateRecords(@RequestParam(name = "c try { // Drop the table mysqlJdbcTemplate.execute("DROP TABLE IF EXISTS patient_records"); - logger.info("Table 'patient_records' dropped successfully."); - - // Recreate the table with MySQL syntax + logger.info("Table 'patient_records' dropped successfully.");// Recreate the table with MySQL syntax String createTableSql = "CREATE TABLE patient_records (" + "id INT AUTO_INCREMENT PRIMARY KEY," + @@ -127,9 +145,7 @@ public ResponseEntity recreateAndPopulateRecords(@RequestParam(name = "c ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; mysqlJdbcTemplate.execute(createTableSql); - logger.info("Table 'patient_records' created successfully."); - - // Populate data + logger.info("Table 'patient_records' created successfully.");// Populate data dataService.populateData(count); return ResponseEntity.ok("Successfully recreated and initiated population of " + count + " patient records."); } catch (Exception e) { @@ -144,9 +160,7 @@ public ResponseEntity runSimulatedQueries( @RequestParam(name = "repetitions", defaultValue = "100") int repetitions ) { long startTime = System.currentTimeMillis(); - int totalOperations = 0; - - for (int queryTypeIndex = 0; queryTypeIndex < uniqueQueriesCount; queryTypeIndex++) { + int totalOperations = 0;for (int queryTypeIndex = 0; queryTypeIndex < uniqueQueriesCount; queryTypeIndex++) { char queryTypeChar = (char) ('A' + queryTypeIndex); String parentSpanName = "PatientBatch_Type" + queryTypeChar; Span typeParentSpan = otelTracer.spanBuilder(parentSpanName).startSpan(); @@ -160,15 +174,11 @@ public ResponseEntity runSimulatedQueries( } finally { typeParentSpan.end(); } - } - - long endTime = System.currentTimeMillis(); + }long endTime = System.currentTimeMillis(); String message = String.format("Executed %d simulated patient query operations in %d ms.", totalOperations, (endTime - startTime)); logger.info(message); return ResponseEntity.ok(message); - } - - private void performObservablePatientOperation(String operationName) { + }private void performObservablePatientOperation(String operationName) { Span span = otelTracer.spanBuilder(operationName) .setSpanKind(SpanKind.CLIENT) .setAttribute("db.system", "mysql") @@ -187,4 +197,4 @@ private void performObservablePatientOperation(String operationName) { span.end(); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordRepository.java b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordRepository.java index 6f57483c173..f35796c4f7d 100644 --- a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordRepository.java @@ -1,5 +1,7 @@ package org.springframework.samples.petclinic.patientrecords; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -9,26 +11,25 @@ import java.time.LocalDateTime; import java.util.List; -@Repository -public interface PatientRecordRepository extends JpaRepository { +@Repositorypublic interface PatientRecordRepository extends JpaRepository { @Query("SELECT pr FROM PatientRecord pr WHERE pr.treatmentType = :treatmentType " + "AND pr.patientWeight >= :minWeight AND pr.patientWeight <= :maxWeight " + "AND pr.visitDate >= :startDate AND pr.visitDate < :endDate " + "AND pr.treatmentCompleted = :treatmentCompleted") - List findByComplexCriteria( + Page findByComplexCriteria( @Param("treatmentType") String treatmentType, @Param("minWeight") Integer minWeight, @Param("maxWeight") Integer maxWeight, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, - @Param("treatmentCompleted") Boolean treatmentCompleted + @Param("treatmentCompleted") Boolean treatmentCompleted, + Pageable pageable ); @Query("SELECT COUNT(1) FROM PatientRecord pr WHERE pr.treatmentType = :treatmentType") int countRecordsByTreatmentType(@Param("treatmentType") String treatmentType); - - @Query("SELECT COUNT(1) FROM PatientRecord pr WHERE pr.treatmentType = :treatmentType " + +@Query("SELECT COUNT(1) FROM PatientRecord pr WHERE pr.treatmentType = :treatmentType " + "AND pr.treatmentCompleted = true") int countCompletedRecordsByTreatmentType(@Param("treatmentType") String treatmentType); -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/resources/db/mysql/patient_records_schema.sql b/src/main/resources/db/mysql/patient_records_schema.sql index 053f81ac221..ef580a805f0 100644 --- a/src/main/resources/db/mysql/patient_records_schema.sql +++ b/src/main/resources/db/mysql/patient_records_schema.sql @@ -12,5 +12,6 @@ CREATE TABLE patient_records ( INDEX idx_treatment_type (treatment_type), INDEX idx_visit_date (visit_date), - INDEX idx_treatment_completed (treatment_completed) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file + INDEX idx_treatment_completed (treatment_completed), + INDEX idx_patient_weight (patient_weight) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file