mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2025-07-17 05:15:50 +00:00
Merge 9264c8b083
into 30aab0ae76
This commit is contained in:
commit
67cddf1c5e
14 changed files with 353 additions and 1 deletions
13
pom.xml
13
pom.xml
|
@ -36,6 +36,7 @@
|
||||||
<maven-checkstyle.version>3.6.0</maven-checkstyle.version>
|
<maven-checkstyle.version>3.6.0</maven-checkstyle.version>
|
||||||
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
|
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
|
||||||
<spring-format.version>0.0.46</spring-format.version>
|
<spring-format.version>0.0.46</spring-format.version>
|
||||||
|
<spring-doc-version>2.5.0</spring-doc-version>
|
||||||
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
@ -135,6 +136,7 @@
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>mysql</artifactId>
|
<artifactId>mysql</artifactId>
|
||||||
|
@ -145,7 +147,16 @@
|
||||||
<groupId>jakarta.xml.bind</groupId>
|
<groupId>jakarta.xml.bind</groupId>
|
||||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>${spring-doc-version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.samples.petclinic;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +27,7 @@ import org.springframework.context.annotation.ImportRuntimeHints;
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
@ImportRuntimeHints(PetClinicRuntimeHints.class)
|
@ImportRuntimeHints(PetClinicRuntimeHints.class)
|
||||||
public class PetClinicApplication {
|
public class PetClinicApplication {
|
||||||
|
|
||||||
|
|
|
@ -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("petDetails");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.springframework.samples.petclinic.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.samples.petclinic.dto.PetDetailDto;
|
||||||
|
import org.springframework.samples.petclinic.exception.ResourceNotFoundException;
|
||||||
|
import org.springframework.samples.petclinic.model.PetDetail;
|
||||||
|
import org.springframework.samples.petclinic.owner.Pet;
|
||||||
|
import org.springframework.samples.petclinic.repository.PetDetailRepository;
|
||||||
|
import org.springframework.samples.petclinic.service.PetDetailService;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/pet-details")
|
||||||
|
@Tag(name = "PetDetails" , description = "Pet details apis")
|
||||||
|
public class PetDetailController {
|
||||||
|
|
||||||
|
private PetDetailService service;
|
||||||
|
private PetDetailRepository petRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public PetDetailController(PetDetailRepository petRepo, PetDetailService service) {
|
||||||
|
this.petRepo = petRepo;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{petId}")
|
||||||
|
@Operation(summary = "create pet details")
|
||||||
|
public ResponseEntity<PetDetail> createDetail(@PathVariable int petId,
|
||||||
|
@RequestBody PetDetailDto dto) {
|
||||||
|
Pet pet = petRepo.findByPetId(petId).get().getPet();
|
||||||
|
PetDetail detail = PetDetail.builder()
|
||||||
|
.pet(pet)
|
||||||
|
.temperament(dto.temperament())
|
||||||
|
.weight(dto.weight())
|
||||||
|
.length(dto.length())
|
||||||
|
.build();
|
||||||
|
return ResponseEntity.ok(service.savePetDetail(detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{petId}")
|
||||||
|
@Operation(summary = "Get pet details by per id")
|
||||||
|
public ResponseEntity<PetDetail> getDetail(@PathVariable int petId) {
|
||||||
|
var detail = service.getPetDetailByPetId(petId);
|
||||||
|
return detail != null ? ResponseEntity.ok(detail) : ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{petId}")
|
||||||
|
@Operation(summary = "Update pet detail")
|
||||||
|
public ResponseEntity<PetDetail> updateDetail(@PathVariable int petId,
|
||||||
|
@RequestBody PetDetailDto dto) {
|
||||||
|
Pet pet = petRepo.findById(petId).orElseThrow(() ->
|
||||||
|
new ResourceNotFoundException("Pet not found with id: " + petId)).getPet();
|
||||||
|
|
||||||
|
PetDetail detail = PetDetail.builder()
|
||||||
|
.pet(pet)
|
||||||
|
.temperament(dto.temperament())
|
||||||
|
.weight(dto.weight())
|
||||||
|
.length(dto.length())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(service.updatePetDetail(petId, detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{petId}")
|
||||||
|
@Operation(summary = "Delete pet detail by pet id")
|
||||||
|
public ResponseEntity<Void> deleteDetail(@PathVariable int petId) {
|
||||||
|
service.deletePetDetail(petId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.springframework.samples.petclinic.dto;
|
||||||
|
|
||||||
|
public record PetDetailDto(
|
||||||
|
String temperament,
|
||||||
|
Double weight,
|
||||||
|
Double length
|
||||||
|
) {
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.springframework.samples.petclinic.exception;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleNotFound(ResourceNotFoundException ex) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("message", "internal server error");
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.springframework.samples.petclinic.exception;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.springframework.samples.petclinic.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.samples.petclinic.owner.Pet;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "pet_details")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class PetDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
@OneToOne
|
||||||
|
@JoinColumn(name = "pet_id", nullable = false)
|
||||||
|
private Pet pet;
|
||||||
|
private String temperament;
|
||||||
|
private Double weight;
|
||||||
|
private Double length;
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.springframework.samples.petclinic.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.expression.spel.ast.OpAnd;
|
||||||
|
import org.springframework.samples.petclinic.model.PetDetail;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PetDetailRepository extends JpaRepository<PetDetail, Integer> {
|
||||||
|
Optional<PetDetail> findByPetId(int petId);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.springframework.samples.petclinic.service;
|
||||||
|
|
||||||
|
import org.springframework.samples.petclinic.dto.PetDetailDto;
|
||||||
|
import org.springframework.samples.petclinic.model.PetDetail;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public interface PetDetailService {
|
||||||
|
PetDetail savePetDetail(PetDetail petDetail);
|
||||||
|
PetDetail getPetDetailByPetId(Integer petId);
|
||||||
|
PetDetail updatePetDetail(Integer petId, PetDetail petDetail);
|
||||||
|
void deletePetDetail(Integer petId);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.springframework.samples.petclinic.service;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.CachePut;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
|
||||||
|
import org.springframework.samples.petclinic.exception.ResourceNotFoundException;
|
||||||
|
import org.springframework.samples.petclinic.model.PetDetail;
|
||||||
|
import org.springframework.samples.petclinic.repository.PetDetailRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PetDetailServiceImpl implements PetDetailService {
|
||||||
|
|
||||||
|
private final PetDetailRepository petRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public PetDetailServiceImpl(PetDetailRepository petRepo) {
|
||||||
|
this.petRepo = petRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public PetDetail savePetDetail(PetDetail petDetail) {
|
||||||
|
return petRepo.save(petDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Cacheable(value = "petDetails", key = "#petId")
|
||||||
|
@Transactional
|
||||||
|
public PetDetail getPetDetailByPetId(Integer petId) {
|
||||||
|
return petRepo.findByPetId(petId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Pet details not found for the pet id : " +petId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CachePut(value = "petDetails", key = "#petId")
|
||||||
|
@Transactional
|
||||||
|
public PetDetail updatePetDetail(Integer petId, PetDetail updatedDetail) {
|
||||||
|
PetDetail existing = petRepo.findByPetId(petId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Pet detail not found for the pet id: " + petId));
|
||||||
|
|
||||||
|
existing.setTemperament(updatedDetail.getTemperament());
|
||||||
|
existing.setWeight(updatedDetail.getWeight());
|
||||||
|
existing.setLength(updatedDetail.getLength());
|
||||||
|
return petRepo.save(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CacheEvict(value = "petDetails", key = "#petId")
|
||||||
|
@Transactional
|
||||||
|
public void deletePetDetail(Integer petId) {
|
||||||
|
PetDetail detail = petRepo.findByPetId(petId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Pet details not found for the for pet id: " + petId));
|
||||||
|
petRepo.delete(detail);
|
||||||
|
}
|
||||||
|
}
|
30
src/main/resources/db/h2/create_petDetails_table.sql
Normal file
30
src/main/resources/db/h2/create_petDetails_table.sql
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE TABLE pet_types (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(80) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pets (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(80) NOT NULL,
|
||||||
|
birth_date DATE,
|
||||||
|
type_id INT,
|
||||||
|
FOREIGN KEY (type_id) REFERENCES pet_types(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE visits (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
pet_id INT NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
description VARCHAR(255),
|
||||||
|
FOREIGN KEY (pet_id) REFERENCES pets(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pet_details (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
pet_id INT NOT NULL UNIQUE,
|
||||||
|
temperament VARCHAR(100),
|
||||||
|
weight DOUBLE,
|
||||||
|
length DOUBLE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (pet_id) REFERENCES pets(id)
|
||||||
|
);
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.springframework.samples.petclinic.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class PetDetailControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetPetDetail() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/pet-details/1"))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package org.springframework.samples.petclinic.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.samples.petclinic.model.PetDetail;
|
||||||
|
import org.springframework.samples.petclinic.owner.Pet;
|
||||||
|
import org.springframework.samples.petclinic.repository.PetDetailRepository;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class PetDetailServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PetDetailService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PetDetailRepository petRepo;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSaveAndFetchPetDetail() {
|
||||||
|
Pet pet = petRepo.findAll().get(0).getPet();
|
||||||
|
|
||||||
|
PetDetail detail = PetDetail.builder()
|
||||||
|
.pet(pet)
|
||||||
|
.temperament("Aggressive")
|
||||||
|
.weight(20.0)
|
||||||
|
.length(30.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
PetDetail saved = service.savePetDetail(detail);
|
||||||
|
Assertions.assertNotNull(saved.getId());
|
||||||
|
|
||||||
|
PetDetail fetched = service.getPetDetailByPetId(pet.getId());
|
||||||
|
Assertions.assertEquals("Aggressive", fetched.getTemperament());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue