스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.
수많은 편리한 기능을 제공하지만 가장 대표적인 기능은 다음과 같다.
기본 기능인 것이다.
스프링 데이터에서는 이렇게 따로따로 제공하는 것을 하나로 합쳐서
요롷게 깔꼼하게 AllinOne으로 제공한다는 것이다.
<S extends T> S save(S entity)
void delete(ID id)
Optional<T> findById(ID id)
Iterable<T> findAll()
long count()
etc...
즉, 1. JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능 제공한다. 뿐만 아니라 공통화 가능한 기능(페이징, 쏘팅, 기타 등등)이 거의 모두 포함되어 있다.
2. CrudRepository 에서 fineOne() findById() 로 변경되었다.
cmd + o
단축키로 한 번 더 눌러서 All Places가 되게 한 다음 JpaRepository를 검색해보면 된다.스프링 데이터 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();
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해준다. 물론 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
and m.name = ?2
물론 그냥 아무 이름이나 사용하는 것은 아니고 다음과 같은 규칙을 따라야 한다.
public interface ItemRepository extends JpaRepository<Item, Long> {
...
}
JpaRepository 인터페이스를 인터페이스 상속 받고, 제네릭에 관리할 <엔티티, 엔티티ID타입> 를 주면 된다.
그러면 JpaRepository 가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.
JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다.
따라서 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
미친거지 진짜 개발을 대충해도 될정도
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);
}
쿼리 메서드 기능 대신에 직접 JPQL을 사용하고 싶을 때는 @Query 와 함께 JPQL을 작성하면 된다. 이때는 메서드 이름으로 실행하는 규칙은 무시된다.
참고로 스프링 데이터 JPA는 JPQL 뿐만 아니라 JPA의 네이티브 쿼리 기능도 지원하는데, JPQL 대신에 SQL을 직접 작성할 수 있다.
위 사진에서
@Param
을 MyBatis꺼가 아니고 spring data껄로 잘 import 해야한다.
안 넣어준다면 JAVA build 환경에 따라 될 수도 있고 안될 수도 있다.
public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
// 이름으로 조회
List<Item> findByItemNameList(String itemName);
// 가격으로 조회
List<Item> findByPriceLessThanEqual(Integer price);
// 이름 + 가격으로 조회
List<Item> findByItemNameLikeAndPriceLessThenEqual(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 를 사용해서 직접 쿼리를 실행하면 된다.
단점은 오타에 굉장히 예민하다.
만약 위 findByItemNameLikeAndPriceLessThenEqual 메서드에서 Price -> Prices 라고 s 한글자만 잘못 붙여도 즉시 에러가 난다.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'itemController' defined in file [/Users/corporationonpoom/Downloads/source/itemservice-db-start/out/production/classes/hello/itemservice/web/ItemController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'hello.itemservice.config.SpringDataJpaConfig': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springDataJpaItemRepository' defined in hello.itemservice.repository.jpa.SpringDataJpaItemRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List hello.itemservice.repository.jpa.SpringDataJpaItemRepository.findByItemNameLikeAndPricesLessThanEqual(java.lang.String,java.lang.Integer)! Reason: Failed to create query for method public abstract java.util.List hello.itemservice.repository.jpa.SpringDataJpaItemRepository.findByItemNameLikeAndPricesLessThanEqual(java.lang.String,java.lang.Integer)! No property 'prices' found for type 'Item'! Did you mean ''price''?; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List hello.itemservice.repository.jpa.SpringDataJpaItemRepository.findByItemNameLikeAndPricesLessThanEqual(java.lang.String,java.lang.Integer)! No property 'prices' found for type 'Item'! Did you mean ''price''?
메서드 이름으로 쿼리를 실행하는 기능은 다음과 같은 단점이 있다.
1. 조건이 많으면 메서드 이름이 너무 길어진다.
2. 조인 같은 복잡한 조건을 사용할 수 없다.
메서드 이름으로 쿼리를 실행하는 기능은 간단한 경우에는 매우 유용하지만, 복잡해지면 직접 JPQL 쿼리를 작성하는 것이 좋다.
쿼리를 직접 실행하려면 @Query 애노테이션을 사용하면 된다.
메서드 이름으로 쿼리를 실행할 때는 파라미터를 순서대로 입력하면 되지만, 쿼리를 직접 실행할 때는 파라미터를 명시적으로 바인딩 해야 한다.
파라미터 바인딩은 @Param("itemName") 애노테이션을 사용하고, 애노테이션의 값에 파라미터 이름을 주면된다.
느낌상은 뭔가 이렇게 그냥 SpringDataJpaItemRepository를 넣어주면 될 것같다. 하지만 사실은 그렇지 않다.
SpringDataJpaItemRepository는 ItemRepository를 implement하지 않기 때문이다.
@Slf4j
@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();
}
}
}
따라서 Repository를 만들 때는 우리가 설정해 둔 interface를 구현하되, 의존성은 SpringDataJpaItemRepository를 받아서 넣어주면 된다.
그럼 당연 JpaRepository에서 제공하는 메서드와 같은 이름, 반환 타입으로 interface를 잘 작성해야할 것이다.
이렇게 중간에서 JpaItemRepository 가 어댑터 역할을 해준 덕분에 ItemService 가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있고 클라이언트인 ItemService 의 코드를 변경하지 않아도 되는 장점이 있다.
@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);
}
}
그리고 이 config를 사용하여 테스트를 진행해보면 에러가 난다.
그리고 얘는 하이버 네이트 버그인데 자세한 이슈 트래킹은 깃에서 보면 된다.
혹 영어가 불편하다면 한국인이 친절하게 이슈를 올려두었다.
대충 요약하면 @Param을 사용할 때 버그들이 일어나는 것 같은데
1. 다운그레이드 하셈
2. @Query로 쿼리문 직접 짜셈 정도 되는 것 같다.
1번으로 해결해보자면 아래 코드를 통하여 특정 라이브러리에 특정 버전을 설정하는데
ext["hibernate.version"] = "5.6.5.Final"
이렇게 추가해주면 된다.
지금(24.01.26)은 hibernate 6 버전 대 와 spring boot도 3 버전 대 가 나왔기 때문에 오래전에 해결된 이슈다.
요는 이런 식으로 이슈를 처리하면 된다는 것이다.