우리는 JPA라는 개념을 배우고 유저 테이블에 JPA를 적용해 보았습니다. 몇 가지 문제를 통해 JPA를 연습해 봅시다! 🔥
library-app-/src/main/java/com/group/fruitshopapp at main · nakyeonko3/library-app- · GitHub
일단 application.yml
파일을 수정했다.
spring:
datasource:
url: "jdbc:mysql://localhost/fruitShop"
username: "root"
password: "***************"
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
dialect
:통해 DB마다 SQL이 조금씩 다른데 이에 맞게 dialect
옵션에 따라 수정해준다.org.hibernate.dialect.MySQL8Dialect
: mysql 버전8에 맞게 SQL문을 만들어준다는 뜻ddl-auto
: 스프링을 시작할 떄 DB를 어떻게 처리할 지에 대한 것이다.ddl-auto: none
: 별다른 조치를 하지 않는다.show_sql:true
: jpa가 쿼리를 할 때 쿼리되는 sql문이 나타나도록한다.format_sql
: 쿼리 할 때 나오는 sql쿼리문을 이쁘게 포매팅해준다.일단 기존 수정하지 않은 fruitshopapp
이 잘되는지 실행을 먼저 해보았다.
에러가 떴다. 아까 application.yml
를 잘못 수정한 것 같다.
yml파일 내용을 frutShop을 fruitShop로 수정했다.
password
도 잘못 입력한 것 같아서 비밀번호도 다시 수정했다.
잘된다 이제 코드를 수정해보자.
FruitServiceV2
라는 서비스 클래스를 새로 하나 만들었다.
기존 서비스 클래스 명은 FruitServiceV1
으로 수정하고, 기존의 리포티토리 클래스도 FruitJdbcRepository
로 수정했다.
새 서비스 클래스에 jpa리포지토리를 연결 해주고,
FruitServiceV2
서비스 클래스의 코드를 하나씩 수정을 했다.
FruitServiceV2.java
소스코드
@Service
public class FruitServiceV2 {
private FruitJpaRepository fruitJpaRepository;
public FruitServiceV2(FruitJpaRepository fruitJpaRepository) {
this.fruitJpaRepository = fruitJpaRepository;
}
public void createFruit(FruitCreateRequest request) {
Fruit fruit = fruitJpaRepository.save(new Fruit(request));
System.out.println(fruit.getId() + ":" + fruit.getName());
}
public void updateFruit(FruitUpdateRequest request) {
Fruit fruit = fruitJpaRepository.findById(request.getId())
.orElseThrow(() -> new IllegalArgumentException(String.format("can't find name:%s", request.getId())));
fruitJpaRepository.save(fruit);
}
public FruitGetStatResponse getStatOfFruit(String name) {
List<Fruit>fruits = fruitJpaRepository.findAllByName(name);
long salesAmount = 0L;
long notSalseAmount = 0L;
for (Fruit fruit1 : fruits) {
if(fruit1.isSold()){
salesAmount += fruit1.getPrice();
} else {
notSalseAmount += fruit1.getPrice();
}
}
return new FruitGetStatResponse(salesAmount, notSalseAmount);
}
}
새 서비스 클래스FruitServiceV2
를 지금 컨트롤러 클래스 FruitController
와 연결 해주었다.
postman으로 fruitshopapp이 에러 없이 잘 실행 되는지 확인
FruitServiceV2.java
서비스 클래스에 getfruitGetCountByIsSold
메서드를 추가했다.
@Service
public class FruitServiceV2 {
private FruitJpaRepository fruitJpaRepository;
public FruitServiceV2(FruitJpaRepository fruitJpaRepository) {
this.fruitJpaRepository = fruitJpaRepository;
}
public void createFruit(FruitCreateRequest request) {
Fruit fruit = fruitJpaRepository.save(new Fruit(request));
System.out.println(fruit.getId() + ":" + fruit.getName());
}
public void updateFruit(FruitUpdateRequest request) {
Fruit fruit = fruitJpaRepository.findById(request.getId())
.orElseThrow(() -> new IllegalArgumentException(String.format("can't find name:%s", request.getId())));
fruitJpaRepository.save(fruit);
}
public FruitGetStatResponse getStatOfFruit(String name) {
List<Fruit>fruits = fruitJpaRepository.findAllByName(name);
long salesAmount = 0L;
long notSalseAmount = 0L;
for (Fruit fruit1 : fruits) {
if(fruit1.isSold()){
salesAmount += fruit1.getPrice();
} else {
notSalseAmount += fruit1.getPrice();
}
}
return new FruitGetStatResponse(salesAmount, notSalseAmount);
}
// 추가된 메서드
public FruitGetCountResponse getfruitGetCountByIsSold(String name){
List<Fruit>fruits = fruitJpaRepository.findAllByName(name);
long count = 0L;
for (Fruit fruit1 : fruits) {
if(fruit1.isSold()){
count ++;
}
}
return new FruitGetCountResponse(count);
}
}
count 결과 값을 담을 FruitGetCountResponse
DTO를 추가함.
@Getter
@RequiredArgsConstructor
public class FruitGetCountResponse {
final private long count;
}
FruitControllerV2
컨트롤러 클래스에 /api/v1/fruit/count
를 매핑하는 GET 메서드를 추가했다.
@Primary
@RequestMapping("/api/v1/fruit")
@RestController
public class FruitControllerV2 {
private FruitServiceV2 fruitServiceV2;
public FruitControllerV2(FruitServiceV2 fruitServiceV2) {
this.fruitServiceV2 = fruitServiceV2;
}
@PostMapping
public void createFruit(@RequestBody FruitCreateRequest request) {
fruitServiceV2.createFruit(request);
}
@PutMapping
public void updateFruit(FruitUpdateRequest request) {
fruitServiceV2.updateFruit(request);
}
@GetMapping("/stat")
public FruitGetStatResponse getStatOfFruit(@RequestParam String name) {
return fruitServiceV2.getStatOfFruit(name);
}
// --------------추가된 메서드 ---------
@GetMapping("/count")
public FruitGetCountResponse getfruitGetCountByIsSold(@RequestParam String name){
return fruitServiceV2.getfruitGetCountByIsSold(name);
}
// --------------추가된 메서드 ---------
}}
}
library-app-/src/main/java/com/group/fruitshopapp at 1c67f5b1d57e21b694331f58c8ca58df79192889 · nakyeonko3/library-app- · GitHub
프로젝션과 jpa 를 이용하면 풀 수 있는 문제이다.
컨트롤러 클래스에 "/list"
매핑을 추가함
FruitController.java
@Primary
@RequestMapping("/api/v1/fruit")
@RestController
public class FruitControllerV2 {
final private FruitServiceV2 fruitServiceV2;
public FruitControllerV2(FruitServiceV2 fruitServiceV2) {
this.fruitServiceV2 = fruitServiceV2;
}
@PostMapping
public void createFruit(@RequestBody FruitCreateRequest request) {
fruitServiceV2.createFruit(request);
}
@PutMapping
public void updateFruit(FruitUpdateRequest request) {
fruitServiceV2.updateFruit(request);
}
@GetMapping("/stat")
public FruitGetStatResponse getStatOfFruit(@RequestParam String name) {
return fruitServiceV2.getStatOfFruit(name);
}
@GetMapping("/count")
public FruitGetCountResponse getfruitGetCountByIsSold(@RequestParam String name){
return fruitServiceV2.getFruitsCountByIsSold(name);
}
// -------추가된 부분-----------
@GetMapping("/list")
public List<FruitGetListProjection> getFruitsPriceList(FruitGetPriceListRequest request){
return fruitServiceV2.getFruitsPriceList(request);
}
// -------추가된 부분-----------
}
option이 GTE라면 findAllByPriceGreaterThan가 실행되고
option이 LTE라면 findAllByPriceLessThan이 실행되게 했다.
FruitServiceV2.java
@Service
public class FruitServiceV2 {
final private FruitJpaRepository fruitJpaRepository;
public FruitServiceV2(FruitJpaRepository fruitJpaRepository) {
this.fruitJpaRepository = fruitJpaRepository;
}
public void createFruit(FruitCreateRequest request) {
Fruit fruit = fruitJpaRepository.save(new Fruit(request));
System.out.println(fruit.getId() + ":" + fruit.getName());
}
public void updateFruit(FruitUpdateRequest request) {
Fruit fruit = fruitJpaRepository.findById(request.getId())
.orElseThrow(() -> new IllegalArgumentException(String.format("can't find name:%s", request.getId())));
fruitJpaRepository.save(fruit);
}
public FruitGetStatResponse getStatOfFruit(String name) {
List<Fruit> fruits = fruitJpaRepository.findAllByName(name);
long salesAmount = 0L;
long notSalseAmount = 0L;
for (Fruit fruit1 : fruits) {
if (fruit1.isSold()) {
salesAmount += fruit1.getPrice();
} else {
notSalseAmount += fruit1.getPrice();
}
}
return new FruitGetStatResponse(salesAmount, notSalseAmount);
}
public FruitGetCountResponse getFruitsCountByIsSold(String name) {
List<Fruit> fruits = fruitJpaRepository.findAllByName(name);
long count = 0L;
for (Fruit fruit1 : fruits) {
if (fruit1.isSold()) {
count++;
}
}
return new FruitGetCountResponse(count);
}
// -------추가된 부분-----------
public List<FruitGetListProjection> getFruitsPriceList(FruitGetPriceListRequest request) {
if (request.getOption().equals("GTE")) {
return fruitJpaRepository.findAllByPriceGreaterThan(request.getPrice());
} else if (request.getOption().equals("LTE")) {
return fruitJpaRepository.findAllByPriceLessThan(request.getPrice());
} else {
throw new IllegalArgumentException();
}
}
// -------추가된 부분-----------
}
FruitJpaRepository.java
@Primary
@Repository
public interface FruitJpaRepository extends JpaRepository<Fruit, Long> {
Optional<Fruit> findById(Long id);
List<Fruit> findAllByName(String name);
// --- 추가된 부분 ----
List<FruitGetListProjection> findAllByPriceGreaterThan(Long price);
List<FruitGetListProjection> findAllByPriceLessThan(Long price);
// --- 추가된 부분 ----
}
프로젝션을 통해 name, price, warehousingData 칼럼만 가져오도록 바꿨다.
FruitGetListProjection
public interface FruitGetListProjection {
String getName();
Long getPrice();
LocalDate getWarehousingDate();
}
save 메서드를 이용해서 sql update 하는 부분이 조금 이해가 안되서 자료를 찾아봤다.
뭔가 직관적이지가 않네
Spring Data - CrudRepository save() Method | Baeldung
모른다. 공부가 필요
sql 속성명을 싹다 카멜케이스에서 스네이크케이스로 변경하니 해결이 되었다.
Spring Boot JPA unknown column in field list
같은 기능을 하는 컨트롤러 클래스 2개를 bean에 등록해서 이런 오류가 발생했다. 컨트롤러 클래스를 하나만 남기니 괜찮아졌다.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'fruitControllerV2' method
com.group.fruitshopapp.controller.FruitControllerV2#updateFruit(FruitUpdateRequest)
to {PUT [/api/v1/fruit]}: There is already 'fruitControllerV1' bean method
com.group.fruitshopapp.controller.FruitControllerV1#updateFruit(FruitUpdateRequest) mapped
find all은 모든 값을 가져오는 방식이라 개선이 필요하다. 너무 많은 데이터를 가져오게 되면 딜레이가 걸릴게 뻔하다.
쿼리문의 결과를 단일 결과물로 예상하지만, 두 개 이상의 결과가 나왔을 이 오류가 발생한다.
fruitJpaRepository.findByName(name) .orElseThrow(() -> new IllegalArgumentException(String.format("can't find name:%s", name)));
2024-02-27 19:36:43.073 ERROR 28072 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 3; nested exception is javax.persistence.NonUniqueResultException: query did not return a unique result: 3] with root cause
내일은 이 두 개념에 대해 공부를 하게 될 예정이다.