스프링 DB 2편 - 데이터 접근 활용 기술 (6)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
데이터 접근기술 - 스프링 데이터 JPA
스프링 데이터 JPA는 JAP를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.
- 공통 인터페이스 기능
- 쿼리 메서드 기능
공통 인터페이스 기능
JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능을 제공한다.
공통화 가능한 기능이 거의 모두 포함되어 있다.
JpaRepository 사용법
public interface ItemRepository extends JpaRepository<Item, Long> {
}
JpaRepository 인터페이스를 인터페이스 상속 받고, 제너릭에 관리할 엔티티, 엔티티ID를 주면 된다.
그러면 JpaRepository가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.
JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다.
그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다.
개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
쿼리 메서드 기능
스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.
순수 JPA 리포지토리
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
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해준다. JPQL은 JPA가 SQL로 번역해서 실행한다.
스프링 데이터 JPA가 제공하는 쿼리 메서드 기능
조회 : find...By
COUNT : count...By 반환타입 long
EXISTS : exists...By 반환타입 boolean
삭제 : delete...By , remove...By 반환타입 long
DISTINCT : findDistinct , findMemberDistinctBy
LIMIT : findFirst3, findFirst , findTop , findTop3
JPQL 직접 사용하기
//쿼리 직접 실행
@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);
쿼리 메서드 기능 대신에 직접 JPQL을 사용하고 싶을 때는 @Query와 함께 JPQL을 작성하면 된다.
스프링 데이터 JPA는 JPQL 뿐만 아니라 JPA의 네이티브 쿼리 기능도 지원하는데, JPQL 대신에 SQL을 직접 작성할 수 있다.
스프링 데이터 JPA 적용
build.gradle 추가
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
여기에는 하이버네이트, 스프링 데이터 JPA , 스프링 JDBC 관련 기능도 모두 포함되어 있다.
SpringDataJpaItemRepository
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 기능을 사용할 수 있다.
그런데 이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 아니다. 따라서 쿼리 메서드 기능을 사용하거나 @Query를 사용해서 직접 쿼리를 실행하면 된다.
findAll()
코드에는 보이지 않지만 JpaRepository 공통 인터페이스가 제공하는 기능이다.
모든 Item을 조회한다.
findByItemNameLike()
이름 조건과 검색했을 때 사용하는 쿼리 메서드 이다.
findByPriceLessThanEqual()
가격 조건만 검색했을 때 사용하는 쿼리 메서드이다.
findByItemNameLikeAndPriceLessThanEqual()
이름과 가격 조건을 검색했을 때 사용하는 쿼리 메서드이다.
findItems()
메서드 이름으로 쿼리를 실행하는 기능은 단점이 있다.
1. 조건이 많으면 메서드 이름이 길어진다.
2. 조인 같은 복잡한 조건을 사용할 수 없다.
복잡해지는 경우라면 직접 JPQL 쿼리를 작성하는 것이 좋다.
쿼리를 직접 실행할 때는 파라미터를 명시적으로 바인딩 해야 한다.
파라미터 바인딩 애노테이션을 사용하고, 애노테이션 값에 파라미터 이름을 주면 된다.
스프링 데이터 적용2
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
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();
}
}
}
save()
스프링 데이터 JPA가 제공하는 save() 호출한다.
update()
스프링 데이터 JPA가 제공하는 findById() 메서드를 사용해서 엔티티를 찾는다.
그리고 데이터를 수정한다.
트랜잭션이 커밋될 때 변경 내용이 데이터베이스에 반영 된다.
findById()
스프링 데이터 JPA가 제공하는 findById() 메서드를 사용해서 엔티티를 찾는다.
findAll()
조건이 2개만 되어도 이름이 너무 길어지는 단점이 있기 때문에 스프링 데이터 JPA가 제공하는 메서드이름으로 쿼리를 자동으로 만들어주는 기능과 @Query 로 직접 쿼리를 작성하는 기능 중에 적절한 선택이 필요하다.
SpringDataJpaConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV1;
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;
import javax.persistence.EntityManager;
@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);
}
}
ItemServiceApplication
@Import(SpringDataJpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {