스프링 DB 2편 - 데이터 접근 활용 기술 (5)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
데이터 접근 기술 - JPA
JPA
- Java Persistence API
- 자바 진영의 ORM 기술 표준
ORM
- Object-relational mapping (객체 관계 매핑)
- 객체는 객체대로 설계
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계
- ORM 프레임워크가 중간에서 매핑
- 대중적인 언어에는 대부분 ORM 기술이 존재
JPA를 사용해야 하는 이유
- SQL 중심적인 개발에서 객체 중심으로 개발
- 생산성
- 유지보수
- 패러다임의 불일치 해결
- 성능
- 데이터 접근 추상화와 벤더 독립성
- 표준
JPA 설정
build.gradle 에 의존관계를 추가한다.
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
spring-boot-start-data-jpa 는 spring-boot-starter-jdbc도 함께 포함한다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
위의 jdbc는 제거한다.
application.properties 에 설정을 추가한다.
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG = 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.
org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩 되는 파라미터를 확인할 수 있다.
JPA적용
package hello.itemservice.domain;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private 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가 인식할 수 있다.
@ID : 테이블의 PK와 해당 필드를 매핑한다.
@GeneratedValue : PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다.
@Column : 객체의 필드를 테이블의 컬럼과 매핑한다.
name = "item_name" 객체는 itemName 이지만 테이블 컬럼은 item_name이므로 이렇게 매핑한다.
length = 10 : JPA의 매핑 정보로 DDL 도 생성할 수있는데, 그때 컬럼의 길이 값으로 활용된다.
@Column을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 객체 필드의 카멜 케이스를 테이블 컬럼의 언 더 스코어로 자동으로 변환해준다.
@Column(name = "item_name")를 생략해도 된다.
JPA는 public , protected의 기본 생성자가 필수이다.
public Item() {}
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 em;
public JpaItemRepositoryV1(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";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
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 = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
private final EntityManager em : JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터 소스를 가지고있고, 데이터베이스에 접근할 수 있다. 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
@Transactional : JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다. 리포지토리에 트랜잭션을 걸어주었지만 보통 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는것이 맞다.
save()
em.persist(item) : JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드를 사용하면 된다.
update() 수정
em.update() 같은 메서드를 전혀 호출하지 않았다. 그런데도 UPDATE SQL이 실행되는 것은
JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티가 있는지 확인한다. 특정 엔티티 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
findById() - 단건조회
JPA에서 엔티티 객체를 PK를 기준으로 조회할 때는 find()를 사용하고 조회 타입과, PK 값을 주면 된다.
그러면 JPA가 다음과 같은 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 변환해준다.
findAll - 목록 조회
JPQL
JPA는 JPQL 이라는 객체지향 쿼리 언어를 제공한다. 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다.
엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다. 엔티티 객체와 속성의 대소문자는 구분해야 한다.
JPQL에서 파라미터는 where price <= :maxprice
파라미터 바인딩은 query.setParameter("maxPrice", maxPrice)
JPA를 사용해도 동적 쿼리 문제가 남아있다. 동적 쿼리는 Querydsl 이라는 기술을 활용하면 깔끔하게 사용할 수 있다.
JPA 적용 - 예외 변환
JPA의 경우 예외가 발생하면 JPA 예외가 발생하게 된다.
EntityManager는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.
@Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.
@Repository의 기능
- Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.
- Repository가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.