diff --git a/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityController.java b/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityController.java index eba926c0870..047b9d113ad 100644 --- a/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityController.java +++ b/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityController.java @@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; @@ -37,7 +38,7 @@ public class ClinicActivityController implements InitializingBean { @Autowired public ClinicActivityController(ClinicActivityDataService dataService, ClinicActivityLogRepository repository, - JdbcTemplate jdbcTemplate) { + @Qualifier("postgresJdbcTemplate") JdbcTemplate jdbcTemplate) { this.dataService = dataService; this.repository = repository; this.jdbcTemplate = jdbcTemplate; diff --git a/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityDataService.java b/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityDataService.java index b73f57a17b6..7790c49e555 100644 --- a/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityDataService.java +++ b/src/main/java/org/springframework/samples/petclinic/clinicactivity/ClinicActivityDataService.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.samples.petclinic.model.ClinicActivityLog; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; @@ -57,9 +58,9 @@ public class ClinicActivityDataService { @Autowired public ClinicActivityDataService(ClinicActivityLogRepository repository, - JdbcTemplate jdbcTemplate, - DataSource dataSource, - PlatformTransactionManager transactionManager) { + @Qualifier("postgresJdbcTemplate") JdbcTemplate jdbcTemplate, + @Qualifier("postgresDataSource") DataSource dataSource, + @Qualifier("postgresTransactionManager") PlatformTransactionManager transactionManager) { this.repository = repository; this.jdbcTemplate = jdbcTemplate; this.dataSource = dataSource; diff --git a/src/main/java/org/springframework/samples/petclinic/config/DatabaseConfig.java b/src/main/java/org/springframework/samples/petclinic/config/DatabaseConfig.java new file mode 100644 index 00000000000..ca054a42775 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/config/DatabaseConfig.java @@ -0,0 +1,40 @@ +package org.springframework.samples.petclinic.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class DatabaseConfig { + + @Primary + @Bean(name = "postgresDataSource") + @ConfigurationProperties("app.datasource.postgres") + public DataSource postgresDataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + + @Bean(name = "mysqlDataSource") + @ConfigurationProperties("app.datasource.mysql") + public DataSource mysqlDataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + + @Primary + @Bean(name = "postgresJdbcTemplate") + public JdbcTemplate postgresJdbcTemplate(@Qualifier("postgresDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean(name = "mysqlJdbcTemplate") + public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/config/MySqlConfig.java b/src/main/java/org/springframework/samples/petclinic/config/MySqlConfig.java new file mode 100644 index 00000000000..d950001f352 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/config/MySqlConfig.java @@ -0,0 +1,50 @@ +package org.springframework.samples.petclinic.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import jakarta.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableTransactionManagement +@EnableJpaRepositories( + entityManagerFactoryRef = "mysqlEntityManagerFactory", + transactionManagerRef = "mysqlTransactionManager", + basePackages = {"org.springframework.samples.petclinic.patientrecords"} +) +public class MySqlConfig { + + @Bean(name = "mysqlEntityManagerFactory") + public LocalContainerEntityManagerFactoryBean mysqlEntityManagerFactory( + EntityManagerFactoryBuilder builder, + @Qualifier("mysqlDataSource") DataSource dataSource) { + + Map properties = new HashMap<>(); + properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect"); + properties.put("hibernate.hbm2ddl.auto", "none"); + properties.put("hibernate.show_sql", false); + + return builder + .dataSource(dataSource) + .packages("org.springframework.samples.petclinic.model") + .persistenceUnit("mysql") + .properties(properties) + .build(); + } + + @Bean(name = "mysqlTransactionManager") + public PlatformTransactionManager mysqlTransactionManager( + @Qualifier("mysqlEntityManagerFactory") EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/config/PostgresConfig.java b/src/main/java/org/springframework/samples/petclinic/config/PostgresConfig.java new file mode 100644 index 00000000000..3850d1ac99a --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/config/PostgresConfig.java @@ -0,0 +1,61 @@ +package org.springframework.samples.petclinic.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import jakarta.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableTransactionManagement +@EnableJpaRepositories( + entityManagerFactoryRef = "postgresEntityManagerFactory", + transactionManagerRef = "postgresTransactionManager", + basePackages = { + "org.springframework.samples.petclinic.owner", + "org.springframework.samples.petclinic.vet", + "org.springframework.samples.petclinic.clinicactivity" + } +) +public class PostgresConfig { + + @Primary + @Bean(name = "postgresEntityManagerFactory") + public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory( + EntityManagerFactoryBuilder builder, + @Qualifier("postgresDataSource") DataSource dataSource) { + + Map properties = new HashMap<>(); + properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); + properties.put("hibernate.hbm2ddl.auto", "none"); + properties.put("hibernate.show_sql", false); + + return builder + .dataSource(dataSource) + .packages( + "org.springframework.samples.petclinic.owner", + "org.springframework.samples.petclinic.vet", + "org.springframework.samples.petclinic.model" + ) + .persistenceUnit("postgres") + .properties(properties) + .build(); + } + + @Primary + @Bean(name = "postgresTransactionManager") + public PlatformTransactionManager postgresTransactionManager( + @Qualifier("postgresEntityManagerFactory") EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/model/PatientRecord.java b/src/main/java/org/springframework/samples/petclinic/model/PatientRecord.java new file mode 100644 index 00000000000..9c9b8474810 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/model/PatientRecord.java @@ -0,0 +1,95 @@ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +/** + * Patient Record entity for MySQL database + * Represents veterinary patient treatment records + */ +@Entity +@Table(name = "patient_records") +public class PatientRecord extends BaseEntity { + + @Column(name = "treatment_type", nullable = false) + private String treatmentType; + + @Column(name = "patient_weight", nullable = false) + private Integer patientWeight; + + @Column(name = "visit_date", nullable = false) + private LocalDateTime visitDate; + + @Column(name = "treatment_completed", nullable = false) + private Boolean treatmentCompleted; + + @Column(name = "medical_notes", columnDefinition = "TEXT") + private String medicalNotes; + + // Default constructor + public PatientRecord() { + } + + // Constructor with all fields + public PatientRecord(String treatmentType, Integer patientWeight, LocalDateTime visitDate, + Boolean treatmentCompleted, String medicalNotes) { + this.treatmentType = treatmentType; + this.patientWeight = patientWeight; + this.visitDate = visitDate; + this.treatmentCompleted = treatmentCompleted; + this.medicalNotes = medicalNotes; + } + + // Getters and Setters + public String getTreatmentType() { + return treatmentType; + } + + public void setTreatmentType(String treatmentType) { + this.treatmentType = treatmentType; + } + + public Integer getPatientWeight() { + return patientWeight; + } + + public void setPatientWeight(Integer patientWeight) { + this.patientWeight = patientWeight; + } + + public LocalDateTime getVisitDate() { + return visitDate; + } + + public void setVisitDate(LocalDateTime visitDate) { + this.visitDate = visitDate; + } + + public Boolean getTreatmentCompleted() { + return treatmentCompleted; + } + + public void setTreatmentCompleted(Boolean treatmentCompleted) { + this.treatmentCompleted = treatmentCompleted; + } + + public String getMedicalNotes() { + return medicalNotes; + } + + public void setMedicalNotes(String medicalNotes) { + this.medicalNotes = medicalNotes; + } + + @Override + public String toString() { + return "PatientRecord{" + + "id=" + getId() + + ", treatmentType='" + treatmentType + '\'' + + ", patientWeight=" + patientWeight + + ", visitDate=" + visitDate + + ", treatmentCompleted=" + treatmentCompleted + + ", medicalNotes='" + medicalNotes + '\'' + + '}'; + } +} \ 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 new file mode 100644 index 00000000000..4873d5bfc19 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordController.java @@ -0,0 +1,190 @@ +package org.springframework.samples.petclinic.patientrecords; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +@RestController +@RequestMapping("/api/patient-records") +public class PatientRecordController implements InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(PatientRecordController.class); + + private final PatientRecordDataService dataService; + private final PatientRecordRepository repository; + private final JdbcTemplate mysqlJdbcTemplate; + + @Autowired + private OpenTelemetry openTelemetry; + + private Tracer otelTracer; + + @Autowired + public PatientRecordController(PatientRecordDataService dataService, + PatientRecordRepository repository, + @Qualifier("mysqlJdbcTemplate") JdbcTemplate mysqlJdbcTemplate) { + this.dataService = dataService; + this.repository = repository; + this.mysqlJdbcTemplate = mysqlJdbcTemplate; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.otelTracer = openTelemetry.getTracer("PatientRecordController"); + } + + @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) { + return ResponseEntity.badRequest().body("Count must be a positive integer."); + } + if (count > 10_000_000) { + return ResponseEntity.badRequest().body("Count too high - maximum 10,000,000 patient records."); + } + try { + dataService.populateData(count); + return ResponseEntity.ok("Successfully initiated population of " + count + " patient records."); + } catch (Exception e) { + 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) { + + logger.info("Querying patient records by weight: {} (repetitions: {})", patientWeight, repetitions); + + String sql = "SELECT id, treatment_type, patient_weight, visit_date, treatment_completed, medical_notes " + + "FROM patient_records WHERE patient_weight = ?"; + + List> lastResults = null; + for (int i = 0; i < repetitions; i++) { + lastResults = mysqlJdbcTemplate.queryForList(sql, patientWeight); + } + + logger.info("Query completed. Found {} records for weight: {}", + lastResults != null ? lastResults.size() : 0, patientWeight); + return lastResults; + } + + @DeleteMapping("/cleanup-records") + public ResponseEntity cleanupRecords() { + logger.info("Received request to cleanup all patient records."); + try { + dataService.cleanupPatientRecords(); + return ResponseEntity.ok("Successfully cleaned up all patient records."); + } catch (Exception e) { + 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") + public ResponseEntity recreateAndPopulateRecords(@RequestParam(name = "count", defaultValue = "6000000") int count) { + logger.info("Received request to recreate and populate {} patient records.", count); + if (count <= 0) { + return ResponseEntity.badRequest().body("Count must be a positive integer."); + } + if (count > 10_000_000) { + return ResponseEntity.badRequest().body("Count too high - maximum 10,000,000 patient records."); + } + 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 + String createTableSql = + "CREATE TABLE patient_records (" + + "id INT AUTO_INCREMENT PRIMARY KEY," + + "treatment_type VARCHAR(255) NOT NULL," + + "patient_weight INT NOT NULL," + + "visit_date TIMESTAMP NOT NULL," + + "treatment_completed BOOLEAN NOT NULL," + + "medical_notes TEXT," + + "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"; + + mysqlJdbcTemplate.execute(createTableSql); + 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) { + logger.error("Error during patient records recreation and population", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error during data recreation and population: " + e.getMessage()); + } + } + + @GetMapping("/run-simulated-queries") + public ResponseEntity runSimulatedQueries( + @RequestParam(name = "uniqueQueriesCount", defaultValue = "3") int uniqueQueriesCount, + @RequestParam(name = "repetitions", defaultValue = "100") int repetitions + ) { + long startTime = System.currentTimeMillis(); + 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(); + + try (Scope scope = typeParentSpan.makeCurrent()) { + for (int execution = 1; execution <= repetitions; execution++) { + String operationName = "SimulatedPatientQuery_Type" + queryTypeChar; + performObservablePatientOperation(operationName); + totalOperations++; + } + } finally { + typeParentSpan.end(); + } + } + + 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) { + Span span = otelTracer.spanBuilder(operationName) + .setSpanKind(SpanKind.CLIENT) + .setAttribute("db.system", "mysql") + .setAttribute("db.name", "petclinic") + .setAttribute("db.statement", "SELECT * FROM some_patient_table" + operationName) + .setAttribute("db.operation", "SELECT") + .startSpan(); + try { + Thread.sleep(ThreadLocalRandom.current().nextInt(1, 6)); + logger.debug("Executing simulated patient operation: {}", operationName); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Simulated patient operation {} interrupted", operationName, e); + span.recordException(e); + } finally { + span.end(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordDataService.java b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordDataService.java new file mode 100644 index 00000000000..1739b9c99e4 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordDataService.java @@ -0,0 +1,179 @@ +package org.springframework.samples.petclinic.patientrecords; + +import com.github.javafaker.Faker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.samples.petclinic.model.PatientRecord; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +public class PatientRecordDataService { + + private static final Logger logger = LoggerFactory.getLogger(PatientRecordDataService.class); + private static final int BATCH_SIZE = 1000; + + private final PatientRecordRepository repository; + private final JdbcTemplate mysqlJdbcTemplate; + private final PlatformTransactionManager mysqlTransactionManager; + + // List of veterinary treatment types + private static final List TREATMENT_TYPES = List.of( + "Annual Wellness Exam", "Vaccination (Rabies)", "Vaccination (DHPP)", "Vaccination (FVRCP)", + "Dental Cleaning", "Dental Extraction", "Spay Surgery", "Neuter Surgery", + "Wound Care", "Emergency Treatment", "X-Ray Examination", "Blood Work", + "Flea Treatment", "Tick Treatment", "Heartworm Prevention", "Microchip Implant", + "Grooming Service", "Nail Trimming", "Ear Cleaning", "Eye Examination", + "Skin Condition Treatment", "Allergy Treatment", "Pain Management", "Post-Surgery Follow-up" + ); + + private final Random random = new Random(); + + @Autowired + public PatientRecordDataService(PatientRecordRepository repository, + @Qualifier("mysqlJdbcTemplate") JdbcTemplate mysqlJdbcTemplate, + @Qualifier("mysqlTransactionManager") PlatformTransactionManager mysqlTransactionManager) { + this.repository = repository; + this.mysqlJdbcTemplate = mysqlJdbcTemplate; + this.mysqlTransactionManager = mysqlTransactionManager; + } + + @Transactional("mysqlTransactionManager") + public void cleanupPatientRecords() { + logger.info("Received request to clean up all patient records."); + long startTime = System.currentTimeMillis(); + try { + repository.deleteAllInBatch(); // Efficiently delete all entries + long endTime = System.currentTimeMillis(); + logger.info("Successfully cleaned up all patient records in {} ms.", (endTime - startTime)); + } catch (Exception e) { + logger.error("Error during patient records cleanup", e); + throw new RuntimeException("Error cleaning up patient records: " + e.getMessage(), e); + } + } + + @Transactional("mysqlTransactionManager") + public void populateData(int totalEntries) { + long startTime = System.currentTimeMillis(); + logger.info("Starting MySQL batch insert data population of {} patient records.", totalEntries); + + try { + populateDataWithMySqlBatchInNewTransaction(totalEntries); + } catch (Exception e) { + logger.error("Error during patient records population", e); + throw new RuntimeException("Error during patient records population: " + e.getMessage(), e); + } + + long endTime = System.currentTimeMillis(); + logger.info("Finished patient records population for {} records in {} ms.", totalEntries, (endTime - startTime)); + } + + private void populateDataWithMySqlBatchInNewTransaction(int totalEntries) { + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionStatus status = mysqlTransactionManager.getTransaction(def); + + try { + Faker faker = new Faker(new Locale("en-US")); + + // MySQL-specific batch insert using multiple VALUES + String sql = "INSERT INTO patient_records (treatment_type, patient_weight, visit_date, treatment_completed, medical_notes) VALUES "; + + for (int i = 0; i < totalEntries; ) { + StringBuilder batchSql = new StringBuilder(sql); + List batchParams = new ArrayList<>(); + + int batchCount = 0; + for (int j = 0; j < BATCH_SIZE && i < totalEntries; j++, i++, batchCount++) { + if (j > 0) { + batchSql.append(", "); + } + batchSql.append("(?, ?, ?, ?, ?)"); + + String treatmentType = TREATMENT_TYPES.get(random.nextInt(TREATMENT_TYPES.size())); + int patientWeight = faker.number().numberBetween(500, 50_000); // 0.5kg to 50kg in grams + LocalDateTime visitDate = LocalDateTime.ofInstant( + faker.date().past(2 * 365, TimeUnit.DAYS).toInstant(), ZoneId.systemDefault()); + boolean treatmentCompleted = faker.bool().bool(); + String medicalNotes = generateVeterinaryNotes(faker, treatmentType); + + batchParams.add(treatmentType); + batchParams.add(patientWeight); + batchParams.add(visitDate); + batchParams.add(treatmentCompleted); + batchParams.add(medicalNotes); + } + + if (batchCount > 0) { + mysqlJdbcTemplate.update(batchSql.toString(), batchParams.toArray()); + + if (logger.isInfoEnabled()) { + logger.info("MySQL batch inserted {} / {} patient records...", i, totalEntries); + } + } + } + + mysqlTransactionManager.commit(status); + + } catch (Exception e) { + if (!status.isCompleted()) { + mysqlTransactionManager.rollback(status); + } + logger.error("Error during MySQL batch population with new transaction", e); + throw new RuntimeException("Error during MySQL batch population: " + e.getMessage(), e); + } + } + + private String generateVeterinaryNotes(Faker faker, String treatmentType) { + StringBuilder notes = new StringBuilder(); + + // Treatment-specific notes + switch (treatmentType) { + case "Annual Wellness Exam": + notes.append("Patient appears healthy. Weight within normal range. "); + notes.append("Heart rate: ").append(faker.number().numberBetween(60, 120)).append(" bpm. "); + notes.append("Temperature: ").append(faker.number().numberBetween(100, 103)).append("°F. "); + break; + case "Dental Cleaning": + notes.append("Dental tartar buildup noted. Cleaning performed under anesthesia. "); + notes.append("Extracted ").append(faker.number().numberBetween(0, 3)).append(" damaged teeth. "); + break; + case "Vaccination (Rabies)": + case "Vaccination (DHPP)": + case "Vaccination (FVRCP)": + notes.append("Vaccination administered successfully. "); + notes.append("Next vaccination due in 1 year. "); + notes.append("No adverse reactions observed. "); + break; + case "Emergency Treatment": + notes.append("Emergency case: ").append(faker.medical().symptoms()).append(". "); + notes.append("Immediate treatment provided. "); + break; + default: + notes.append("Treatment performed as scheduled. "); + notes.append("Patient responded well to procedure. "); + } + + // Add general veterinary observations + notes.append("Owner compliance: ").append(faker.options().option("Excellent", "Good", "Fair")); + notes.append(". Follow-up: ").append(faker.options().option("Not needed", "1 week", "2 weeks", "1 month")); + notes.append(". Additional notes: ").append(faker.lorem().sentence()); + + return notes.toString(); + } +} \ 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 new file mode 100644 index 00000000000..6f57483c173 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordRepository.java @@ -0,0 +1,34 @@ +package org.springframework.samples.petclinic.patientrecords; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.samples.petclinic.model.PatientRecord; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public 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( + @Param("treatmentType") String treatmentType, + @Param("minWeight") Integer minWeight, + @Param("maxWeight") Integer maxWeight, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("treatmentCompleted") Boolean treatmentCompleted + ); + + @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 " + + "AND pr.treatmentCompleted = true") + int countCompletedRecordsByTreatmentType(@Param("treatmentType") String treatmentType); +} \ No newline at end of file diff --git a/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordUiController.java b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordUiController.java new file mode 100644 index 00000000000..2a224889905 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/patientrecords/PatientRecordUiController.java @@ -0,0 +1,26 @@ +package org.springframework.samples.petclinic.patientrecords; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/patient-records") +public class PatientRecordUiController { + + private final JdbcTemplate mysqlJdbcTemplate; + + @Autowired + public PatientRecordUiController(@Qualifier("mysqlJdbcTemplate") JdbcTemplate mysqlJdbcTemplate) { + this.mysqlJdbcTemplate = mysqlJdbcTemplate; + } + + @GetMapping("/query-records") + public String showQueryRecordsPage() { + return "patientrecords/query-records"; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f7aef094b4f..93b913b92dd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,6 +6,24 @@ database=h2 spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql spring.sql.init.data-locations=classpath*:db/${database}/data.sql +# PostgreSQL DataSource Configuration +app.datasource.postgres.jdbc-url=jdbc:postgresql://localhost:5442/petclinic +app.datasource.postgres.username=${POSTGRES_USER:postgres} +app.datasource.postgres.password=${POSTGRES_PASS:postgres} +app.datasource.postgres.driver-class-name=org.postgresql.Driver +app.datasource.postgres.maximum-pool-size=50 +app.datasource.postgres.minimum-idle=10 +app.datasource.postgres.connection-timeout=20000 + +# MySQL DataSource Configuration +app.datasource.mysql.jdbc-url=${MYSQL_URL:jdbc:mysql://rds-mysql.cicntohgqj9u.eu-west-1.rds.amazonaws.com:3306/petclinic-mysql} +app.datasource.mysql.username=${MYSQL_USER:admin} +app.datasource.mysql.password=${MYSQL_PASS:NIO9-?sGfibDi*VPeyZ0yJYd>22)} +app.datasource.mysql.driver-class-name=com.mysql.cj.jdbc.Driver +app.datasource.mysql.maximum-pool-size=30 +app.datasource.mysql.minimum-idle=5 +app.datasource.mysql.connection-timeout=20000 + # Web spring.thymeleaf.mode=HTML diff --git a/src/main/resources/db/mysql/patient_records_schema.sql b/src/main/resources/db/mysql/patient_records_schema.sql new file mode 100644 index 00000000000..053f81ac221 --- /dev/null +++ b/src/main/resources/db/mysql/patient_records_schema.sql @@ -0,0 +1,16 @@ +-- MySQL schema for patient records table + +DROP TABLE IF EXISTS patient_records; + +CREATE TABLE patient_records ( + id INT AUTO_INCREMENT PRIMARY KEY, + treatment_type VARCHAR(255) NOT NULL, + patient_weight INT NOT NULL, + visit_date TIMESTAMP NOT NULL, + treatment_completed BOOLEAN NOT NULL, + medical_notes TEXT, + + 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 diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index f0de63cfc9d..362af46f1f5 100755 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -71,6 +71,11 @@ Activity +
  • + + Patient Records +
  • + diff --git a/src/main/resources/templates/patientrecords/query-records.html b/src/main/resources/templates/patientrecords/query-records.html new file mode 100644 index 00000000000..88ad330ec74 --- /dev/null +++ b/src/main/resources/templates/patientrecords/query-records.html @@ -0,0 +1,171 @@ + + + + + +

    Patient Records Query

    + +
    +
    +
    +

    Query Patient Records by Weight

    +

    + Search patient records by weight in grams. +

    + +
    +
    + + + Common weights: Small cat: 3000g, Medium dog: 15000g, Large dog: 30000g +
    + +
    + + + Number of times to repeat the same query +
    + + + +
    +
    +
    +
    + +
    +
    + + + + + +
    +
    + + + + + + \ No newline at end of file