라이브러리 추가
immplementation 'org.springframwork.boot:spring-boot-starter-data-jpa'
라이브러리를 추가하게되면 라이브러리에 hibernate(구현체), persistence(jpa) 등이 들어오게 된다.
로그 설정
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
- 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)
JPA 적용
JPA에서 가장 중요한 것은 객체와 테이블을 매핑하는 것이다. JPA에서 제공하는 애노테이션을 통해 매핑할 수 있다.
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @Entity
- JPA가 사용하는 객체, 이 애노테이션을 붙여야 JPA가 인식할 수있다.
- @Entity가 붙은 객체를 JPA에서는 엔티티라 한다.
- @Id
- 테이블의 PK와 해당 필드를 매핑한다.
- @GeneratedValue
- PK 생성 값을 DB에서 생성하는 'IDENTITY' 방식 사용
- @Column
- 객체와 필드를 테이블의 컬럼과 매핑
- 이를 생략할경우 필드의 이름을 테이블 컬럼 이름으로 사용한다.
- 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동으로 변환해준다.
- 'name = '
- 객체와 필드가 다를때 지정
- 'length = 10 '
- JPA의 매핑 정보로 DDL도 생성할 수 있는데, 그때 컬럼 길이 값으로 활용
- JPA는 public또는 protected의 기본 생성자가 필수이다.
Repository
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.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
@Repository
@Transactional
public class JpaItemRepository implements ItemRepository {
private final EntityManager em;
public JpaItemRepository(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = " SELECT i FROM Item i ";
/*전체 조회
List<Item> result = em.createQuery(jpql, Item.class)
.getResultList();*/
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if(StringUtils.hasText(itemName) || maxPrice != null){
jpql += " where";
}
boolean andFlag = false;
List<Object> param = new ArrayList<>();
if(StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%', :itemName, '%')";
param.add(itemName);
andFlag = true;
}
if(maxPrice != null){
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
param.add(maxPrice);
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if(StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if(maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
- EntityManager em
- 생성자로 DI 받는다. JPA의 모든 동작은 이를 통해 이루어진다. 이는 내부에 데이터소스를 가지고 있어 데이터베이스에 접근 가능하다.
- @Transactional
- JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다. (보통 서비스계층)
- JPA를 설정하려면 EntityManagerFactory, JPATransactionManager, DataSource 등 다양한 설정을 해야하지만
스프링 부트는 이 과정을 모두 자동화 해준다. - JpaBaseConfiguration 참조
save() - 저장
public Item save(Item item) {
em.persist(item);
return item;
}
em.persist(item): JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저에서 제공하는 persist() 메서드를 사용하면 된다.
JPA에서 만들어서 실행하는 SQL
insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
또는
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
또는
insert into item (item_name, price, quantity) values (?, ?, ?)
JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다
Update() - 수정
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
JPA가 만들어서 실행한 SQL
update item set item_name=?, price=?, quantity=? where id=?
- em.update() 같은 메서드를 전혀 호출하지 않았다. 그런데 어떻게 UPDATE SQL이 실행되는 것일까?
- JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인한다. 특정 엔티티 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
- JPA가 어떻게 변경된 엔티티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를 이해해야 한다. 이 부분은 JPA 기본편에서 자세히 다룬다. 지금은 트랜잭션 커밋 시점에 JPA가 변경된 엔티티 객체를 찾아서 UPDATE SQL을 수행한다고 이해하면 된다.
- 테스트의 경우 마지막에 트랜잭션이 롤백되기 때문에 JPA는 UPDATE SQL을 실행하지 않는다. 테스트에서UPDATE SQL을 확인하려면 @Commit 을 붙이면 확인할 수 있다.
findById() - 단건 조회
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
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=?
JPA에서 엔티티 객체를 PK를 기준으로 조회할 때는 find() 를 사용하고 조회 타입과, PK 값을 주면 된다. 그러면 JPA가 다음과 같은 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 변환해준다.
JPA(하이버네이트)가 만들어서 실행한 SQL은 별칭이 조금 복잡하다. 조인이 발생하거나 복잡한 조건에서도 문제 없도
록 기계적으로 만들다 보니 이런 결과가 나온 듯 하다.
findAll() - 목록 조회
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
//동적 쿼리 생략
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
return query.getResultList();
}
JPQL
JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 제공한다.
주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다 생각하면 된다.
엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다. 엔티티 객체와 속성의 대소문자는 구분해야 한다.
JPQL은 SQL과 문법이 거의 비슷하기 때문에 개발자들이 쉽게 적응할 수 있다.
결과적으로 JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
- JPQL에서 파라미터
- where price <= :maxPrice
- 파라미터 바인딩은 다음과 같이 사용한다.
- query.setParameter("maxPrice", maxPrice)
동적 쿼리 문제
JPA를 사용해도 동적 쿼리 문제가 남아있다. 동적 쿼리는 뒤에서 설명하는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있다. 실무에서는 동적 쿼리 문제 때문에, JPA 사용할 때 Querydsl도 함께 선택하게 된다.
'Spring > JDBC' 카테고리의 다른 글
[Spring DB] 스프링 데이터 JPA - 1 (등장 이유와 기능) (0) | 2024.08.19 |
---|---|
[Spring DB] JPA - 3 예외 변환 (0) | 2024.08.19 |
[Spring DB] JPA - 1(ORM 개념) (0) | 2024.08.19 |
[Spring] 데이터 접근 기술 테스트 (0) | 2024.08.14 |
[Spring DB] Mybatis (0) | 2024.08.12 |