7일차 JPA를 왜 사용하는가_어떻게 JPA 사용하는가

nakyeonko3·2024년 2월 27일
1
post-thumbnail

우리는 JPA라는 개념을 배우고 유저 테이블에 JPA를 적용해 보았습니다. 몇 가지 문제를 통해 JPA를 연습해 봅시다! 🔥

JPA 관련 미션 문제1, 문제2, 문제3 풀기


전체 소스코드

library-app-/src/main/java/com/group/fruitshopapp at main · nakyeonko3/library-app- · GitHub

문제1


일단 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이 에러 없이 잘 실행 되는지 확인


문제1 소스 코드

lfruitshopapp- · GitHub

문제2


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);
    }
// --------------추가된 메서드 ---------
}}
}

실행결과

문제3


문제3 소스코드

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();
}

실행결과

메모


CrudRepository.save() 메서드

save 메서드를 이용해서 sql update 하는 부분이 조금 이해가 안되서 자료를 찾아봤다.
뭔가 직관적이지가 않네
Spring Data - CrudRepository save() Method | Baeldung

그러고 보니 JPA는 인터페이스를 참조해서 사용하던데 그 이유가 뭘까?

모른다. 공부가 필요

에러 Spring Boot JPA unknown column in field list


sql 속성명을 싹다 카멜케이스에서 스네이크케이스로 변경하니 해결이 되었다.
Spring Boot JPA unknown column in field list

에러 There is already 'fruitControllerV1' bean method

같은 기능을 하는 컨트롤러 클래스 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

findAllByName을 개선 할 방법 필요

find all은 모든 값을 가져오는 방식이라 개선이 필요하다. 너무 많은 데이터를 가져오게 되면 딜레이가 걸릴게 뻔하다.

에러 query did not return a unique result

쿼리문의 결과를 단일 결과물로 예상하지만, 두 개 이상의 결과가 나왔을 이 오류가 발생한다.
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

영속성, 트랜잭션

내일은 이 두 개념에 대해 공부를 하게 될 예정이다.

참고


profile
웹개발자를 지망하고 있는 대학생, 진순파

0개의 댓글