1. Spring Data JPA
✔ JPA를 편리하게 사용하기 위한 라이브러리
1) 발전과정
(1) 순수 JDBC
(2) Spring JdbcTemplate
(3) Spring + JPA
(4) Spring Data JPA
-> JpaRepository 인터페이스
2) Spring Data JPA 주요 기능
(1) 공통 인터페이스
public interface ItemRepository extends JpaRepository<Item, Long> {
}
- JpaRepository 인터페이스를 통해 기본적인 CRUD 기능 제공
- 공통화 가능한 기능이 거의 모두 포함 (페이징 처리도 포함)
- 사용법 : JpaRepository 인터페이스를 상속받고, 제네릭에 관리할 <엔티티, 엔티티 PK Id>
-> 동적 프록시 기술을 사용해서 스프링 데이터 JPA가 알아서 구현 클래스 생성 + 스프링 빈 등록해줌
-> 개발자는 구현 클래스 없이 인터페이스만 만들면 CRUD 기능 사용 가능
(2) 쿼리 메소드
인터페이스의 메소드 이름으로 쿼리 자동 생성
- 순수 JPA 리포지토리
- 직접 JPQL 작성, 파라미터 직접 바인딩
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
}
- 스프링 데이터 JPA (1) - 메소드 이름으로 쿼리 생성
- 메소드 이름 분석 -> JPQL 생성 -> JPA가 SQL로 번역해서 실행
public interface MemberRepository extends Repository<Member, Long> {
List<User> findByEmailAndName(String email, String name);
}
// 생성된 JPQL
select m from Member m
where m.email = ?1
ans m.name = ?2
* 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능 (쿼리 메소드 필터 조건)
- 조회 : find...By, read...By, query...By, get...By
- count : count...By (반환타입 long)
- exists : exists...By (반환타입 boolean)
- 삭제 : delete...By, remove...By (반환타입 long)
- distinct : findDistinct, findMemberDistinctBy
- limit : findFirst3, findFirst, findTop, findTop3 ...
- 스프링 데이터 JPA (2) - JPQL 직접 사용 (@Query)
- 쿼리 메소드 기능 대신 직접 JPQL을 사용하고 싶을 때는 @Query + JPQL 작성
(ex. 파라미터로 넘길 게 많아서 메소드 이름이 길어지는 경우) - JPA 네이티브 쿼리 기능 - JPQL 대신 SQL을 직접 작성할 수 있음
- 쿼리 메소드 기능 대신 직접 JPQL을 사용하고 싶을 때는 @Query + JPQL 작성
public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
//쿼리 메서드 기능
List<Item> findByItemNameLike(String itemName);
//쿼리 직접 실행
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName, @Param("price") Integer price);
//JPA 네이티브 쿼리 기능
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?0", nativeQuery = true)
User findByEmailAddress(String emailAddress);
}
4) Spring Data JPA 장점
* 백엔드 주요 프레임워크 (데이터 접근 기술 혼합해서 사용)
(1) Spring Data JPA 장점
- 코딩량 ↓
- 편리함
- 도메인 클래스 중요하게 다룸
- 비즈니스 로직에 집중
- 더 많은 테스트 케이스 작성 가능
💡 Spring Data JPA 주의할 점
JPA(하이버네이트)에 대한 이해 선행 필수 !!!
2. 스프링 데이터 JPA 적용
- Service를 깨끗하게 유지하기 위해 ItemRepository 인터페이스에 의존
- ItemRepository 인터페이스를 구현한 JpaItemRepositoryV2 -> 얘를 Service에서 사용
- JPA를 편리하게 사용하기 위한 JpaRepository 인터페이스를 구현한 SpringDataJpaItemRepository
- JpaItemRepositoryV2에서 SpringDataJpaItemRepository 프록시를 주입받아서 사용
(프록시에서 예외 변환도 처리해줌 -> @Repository가 없어도 예외 변환됨)
1) build.gradle 설정
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2) SpringDataJpaItemRepository - 스프링 데이터 JPA의 JpaRepository 인터페이스 상속
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
// 이름으로 조회
List<Item> findByItemNameLike(String itemName);
// 가격으로 조회
List<Item> findByPriceLessThanEqual(Integer price);
// 이름 + 가격 조회 (쿼리 메소드)
List<Item> findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);
// 이름 + 가격 조회 (쿼리 직접 실행)
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName, @Param("price") Integer price);
}
- 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스를 상속 -> 기본 CRUD 알아서 제공
- 이름으로 검색, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 X -> 쿼리 메소드 or @Query 사용해서 직접 쿼리 실행
- findAll()
- JpaRepository 공통 인터페이스가 기본으로 제공
- JPQL : select i from Item i
- findByItemNameLike()
- 쿼리 메소드
- JPQL : select i from Item i where i.name like ?
- findByPriceLessThanEqual()
- 쿼리 메소드
- JPQL : select i from Item i where i.price <= ?
- findByItemNameLikeAndPriceLessThanEqual()
- 쿼리 메소드 -> 조건이 많아서 메소드 이름이 너무 길어짐 + 조인같은 복잡한 조건 사용 X
-> 직접 JPQL 쿼리 작성하는 것이 나음 - JPQL : select i from Item i where i.itemName like ? and i.price <= ?
- 쿼리 메소드 -> 조건이 많아서 메소드 이름이 너무 길어짐 + 조인같은 복잡한 조건 사용 X
- findItems()
- @Query : 쿼리 직접 실행
- @Param("itemName") : 파라미터 명시적 바인딩
3) JpaItemRepositoryV2 - ItemRepository 인터페이스 구현
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {
private final SpringDataJpaItemRepository repository;
@Override
public Item save(Item item) {
return repository.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = repository.findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return repository.findById(id);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
if (StringUtils.hasText(itemName) && maxPrice != null) {
// return repository.findByItemNameLikeAndPriceLessThanEqual("%" + itemName + "%" + maxPrice);
return repository.findItems("%" + itemName + "%", maxPrice);
} else if (StringUtils.hasText(itemName)) {
return repository.findByItemNameLike("%" + itemName + "%");
} else if (maxPrice != null) {
return repository.findByPriceLessThanEqual(maxPrice);
} else {
return repository.findAll();
}
}
}
- ItemService에서 사용하기 위한 Repository (Service의 ItemRepository 의존 유지) -> Service의 코드 변경 X
- save()
- 스프링 데이터 JPA가 제공하는 save() 호출
- 스프링 데이터 JPA가 제공하는 save() 호출
- update()
- 스프링 데이터 JPA가 제공하는 findById() 호출 후 수정
- 트랜잭션이 커밋될 때 변경 내용이 데이터베이스에 반영
- findById()
- 스프링 데이터 JPA가 제공하는 findById() 호출
- 스프링 데이터 JPA가 제공하는 findById() 호출
- findAll()
- 상황에 따라 직접 생성한 쿼리 / 스프링 데이터 JPA가 제공하는 쿼리 선택해서 사용
- -> 동적 쿼리 지원 약함 -> Querydsl 사용
4) Config 설정
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV2;
import hello.itemservice.repository.jpa.SpringDataJpaItemRepository;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class SpringDataJpaConfig {
private final SpringDataJpaItemRepository springDataJpaItemRepository;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV2(springDataJpaItemRepository);
}
}
- 스프링 데이터 JPA가 SpringDataJpaItemRepository를 프록시 기술로 만들어줌 + 스프링 빈으로 등록
5) ItemServiceApplication
@Slf4j
@Import(SpringDataJpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication { }
6) Service - 코드 수정 X
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class ItemServiceV1 implements ItemService {
private final ItemRepository itemRepository;
@Override
public Item save(Item item) {
return itemRepository.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
itemRepository.update(itemId, updateParam);
}
@Override
public Optional<Item> findById(Long id) {
return itemRepository.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCond cond) {
return itemRepository.findAll(cond);
}
}
7) 테스트
🚨 하이버네이트 버그
스프링 부트 2.6.5 버전은 하이버네이트 5.6.7을 사용
-> 하이버네이트 5.6.6 ~ 5.6.7 을 사용하면 Like 문장을 사용할 때 다음 예외가 발생
java.lang.IllegalArgumentException: Parameter value [\] did not match expected type [java.lang.String (n/a)]
build.gradle에 다음을 추가해서 하이버네이트 버전을 문제가 없는 5.6.5.Final 로 맞추기
ext["hibernate.version"] = "5.6.5.Final"
'Spring' 카테고리의 다른 글
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (6) SpringData JPA + Query DSL (0) | 2023.07.18 |
---|---|
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (5) Query DSL (0) | 2023.07.17 |
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (3) JPA (0) | 2023.07.10 |
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (2) MyBatis (0) | 2023.07.06 |
[인프런/스프링 DB 2편] 3. 데이터 접근 기술 - 테스트와 DB 연동 (0) | 2023.06.30 |