728x90
- 스프링: DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공
- JPA: ORM 데이터 접근 기술을 제공
- JdbcTemplate, MyBatis 같은 SQL Mapper 기술은 SQL을 개발자가 직접 작성해야 한다.
- JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해준다.
- 스프링 데이터 JPA, Querydsl 라는 JPA를 편리하게 사용하도록 도와주는 기술을 함께 사용한다.
설정
- spring-boot-starter-data-jpa 라이브러리를 사용
- spring-boot-starter-jdbc 도 함께 포함한다.
- 다음과 같은 라이브러리가 추가됨
- hibernate-core: JPA 구현체인 하이버네이트 라이브러리
- jakarta.persistence-api: JPA Interface
- spring-data-jpa: 스프링 데이터 JPA 라이브러리
- application.properties 에 설정 추가
- logging.level.org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL 확인
- logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩되는 파라미터를 확인할 수 있다.
- spring.jpa.show-sql=true : System.out 콘솔을 통해서 SQL이 출력된다. ( 권장 X )
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.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;
@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager entityManager;
public JpaItemRepositoryV1(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Item save(Item item) {
entityManager.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item foundItem = entityManager.find(Item.class, itemId);
foundItem.setItemName(updateParam.getItemName());
foundItem.setPrice(updateParam.getPrice());
foundItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = entityManager.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%', :itemName, '%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("생성된 jpql = {}", jpql);
TypedQuery<Item> query = entityManager.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
- EntityManager : JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 내부에 DataSource 를 가지고 있고, 데이터베이스에 접근할 수 있다.
- @Transactional : JPA의 모든 데이터 변경은 Transaction 안에서 이루어져야 한다.
- JPA를 설정하려면 EntityManagerFactory, JpaTransactionManager, DataSource 등 다양한 설정을 해야 한다. 스프링 부트는 이 과정을 모두 자동화 해준다. 스프링 부트의 자동 설정은 JpaBaseConfiguration을 참고한다.
save
- persist(item) : JPA에서 객체를 테이블에 저장할 때, EntityManager가 제공하는 persist 메서드를 사용한다.
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
- JPA가 만들어서 실행한 SQL로 id에 값이 빠져있다.
- PK key 생성 전략을 IDENTITY로 사용했기 때문이다.
- 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK 값이 들어간다.
- JPA가 Insert SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다.
update
- Item foundItem = entityManager.find(Item.class, itemId);
- foundItem.setItemName(updateParam.getItemName());
2023-03-05 19:26:41.524 DEBUG 8190 --- [nio-8080-exec-6] org.hibernate.SQL : update item set item_name=?, price=?, quantity=? where id=?
2023-03-05 19:26:41.529 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [B]
2023-03-05 19:26:41.529 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [INTEGER] - [2000]
2023-03-05 19:26:41.530 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [INTEGER] - [80]
2023-03-05 19:26:41.530 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [BIGINT] - [18]
- JPA는 Transaction 이 Commit 되는 시점에 변경된 Entity 객체가 있는지 확인한다.
- 특정 Entity 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
- 어떻게 변경된 객체를 찾는지 이해하려면 영속성 컨텍스트라는 JPA 내부 원리에 대한 이해가 필요하다.
- 테스트의 경우 마지막에 Transaction Rollback이 되기때문에 JPA는 UPDATE SQL을 실행하지 않는다.
- @Commit 을 붙여야 확인이 가능하다.
단건 조회
- JPA에서 객체를 PK를 기준으로 조회할 때는 find 메서드를 사용하고, 조회 타입과 PK 값을 인자로 주면 된다.
- JPA가 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 변환해준다.
select item0_.id as id1_0_0_, item0_.item_name as item_nam2_0_0_, item0_.price as price3_0_0_, item0_.quantity as quantity4_0_0_ from item item0_ where item0_.id=?
binding parameter [1] as [BIGINT] - [18]
목록 조회
String jpql = "select i from Item i";
//동적 쿼리
TypeQuery<Item> query = entityManager.createQuery(jpql, Item.class);
return query.getResultList();
- JPQL ( Java Persistence Query Language )
- JPA는 JPQL이라는 객체지향 쿼리 언어를 제공한다.
- 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
- SQL은 테이블을 대상으로 한다.
- JPQL은 Entity 객체를 대상으로 SQL을 실행한다.
- JPQL을 실행하면 그 안에 포함된 Entity 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
- 파라미터 입력
- where price <= :maxPrice
- 파라미터 바인딩
- query.setParameter("maxPrice", maxPrice);
- 동적 쿼리는 Querydsl 라는 기술을 활용한다.
예외 변환
JPA의 경우 예외가 발생하면 JPA 예외가 발생한다.
- EntityManager는 순수한 JPA 기술이다.
- JPA는 PersistenceException과 그 하위 예외를 발생시킨다.
- JPA 예외를 스프링 예외 추상황 ( DataAccessException ) 으로 변환하는 방법
- @Repository 사용
- 컴포넌트 스캔의 대상이 되고, 예외 변환 AOP 의 적용 대상이 된다.
- 스프링과 JPA를 함께 사용하는 경우, 스프링은 JPA 예외 변환기 ( PersistenceExceptionTranslator ) 를 등록한다.
- 결과적으로 리포지토리에 @Repository 애노테이션만 있으면, 스프링이 예외 변환을 처리하는 AOP를 만들어준다.
- 스프링 부트는 PersistenceExceptionTranslatorPostProcessor 를 자동으로 등록하는데, 여기에서 @Repository 를 AOP Proxy로 만드는 Advisor가 등록된다.
- 실제 JPA 예외를 변환하는 코드는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible() 이다.
728x90
'Tech > 데이터 접근 기술' 카테고리의 다른 글
트랜잭션 매니저 선택 (0) | 2023.03.07 |
---|---|
실용적인 구조 (0) | 2023.03.07 |
스프링 데이터 JPA 소개 (0) | 2023.03.07 |
MyBatis 소개 (0) | 2023.03.05 |
스프링 JdbcTemplate 소개 (0) | 2023.03.05 |