기존의 데이터베이스 문제점
1. Jdbc나 MyBatis,SQL Mapper는 SQL을 개발자가 직접 작성해야 한다.
2. 객체 - 관계의 불일치
- 데이터 저장 방식의 차이
- 객체는 필드(속성) + 메서드(행위) 를 가지지만, 관계형 데이터베이스는 테이블(행, 열) 구조를 가짐.
- 연관관계 표현의 차이
- 객체는 참조(Reference) 를 통해 관계를 표현하지만, RDBMS는 외래키(Foreign Key) 를 사용하여 관계를 표현.
- 이를 수동으로 처리하면 조인(Join)과 SQL 작성이 복잡해짐.
- 상속 구조의 차이
- Java는 클래스 상속(Inheritance) 을 지원하지만, 관계형 데이터베이스는 직접적인 상속 개념이 없음.
- 상속을 표현하기 위해 JOIN 테이블을 사용하거나 단일 테이블 전략 등을 적용해야 함.
이러한 문제를 JPA는 획기적으로 해결해준다.
JPA란?
JPA는 ORM(Object-Relational Mapping) 데이터 접근 기술을 제공하는 자바 엔터프라이즈 시장의 주력 기술이다.
여기서 ORM이란 객체와 데이터베이스를 자동으로 매핑 시켜주는 기술이다.
JPA 장점
- SQL을 개발자가 직접 작성하지 않아도 JPA가 대신 작성하고 처리해준다.
- 복잡하고 어려운 SQL이 아닌 객체(Entity)를 중심으로 데이터를 조작할 수 있기 때문에 유지보수가 쉬워진다.
- SQL을 직접 작성하지 않기 때문에 DBMS에 종속되지 않고 코드를 작성 할 수 있다.
JPA 사용해보기
<의존관계 추가>
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
<application.properties>
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
위 코드를 통해 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.
<Item 도메인>
package hello.itemservice.domain;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // PK를 DB에서 값을 넣어줌
private Long id;
@Column(name="item_name", length=10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() { //JPA는 기본 생성자가 필수이다!!!
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Entity : JPA가 사용하는 객체라는 뜻이다. 해당 애노테이션이 붙은 객체는 관계형 데이터베이스에서 엔티티로 정의된다.
@Id : 테이블의 기본키(PK)에 해당
@GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다.
@Column : 객체의 필드를 테이블의 컬럼과 매핑한다.
★ @Column을 생략하면 필드의 이름이 테이블 컬럼 이름으로 사용된다. (만약 스프링 부트와 통합해서 사용할 경우 카멜 케이스를 테이블 컬럼의 언더스코어로 자동 변환해줌)
<JpaItemRepository>
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 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";
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();
}
}
1. 저장
저장은 EntityManager가 제공하는 persist()메서드를 사용하면 된다.
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
2. 수정
em.update같은 메서드가 필요할 것 같지만 update가 정상적으로 수행된다.
왜냐하면 JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인하고 특정 엔티티 객체가 변경된 경우에 UPDATE SQL을 수행하기 때문이다.
@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());
}
3. 검색
검색의 경우 find()메서드를 사용하면 된다.
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
4. 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();
}
JPA에서는 JPQL이라는 객체지향 쿼리 언어를 제공한다.
이는 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용된다.
※ 주의 : JPQL은 SQL과 달리 대소문자 구분을 한다!!
순수 JPA만을 사용하면 동적 쿼리의 복잡성 문제가 아직 해결 안된다.
동적쿼리를 깔끔하게 사용하기 위해서는 Querydsl을 사용하면 된다.( 다음 포스팅에 업로드 예정 )
JPA는 데이터에 쉽게 접근할 수 있으며 무엇보다 SQL을 자동으로 작성해준다는 점에서 뛰어난 장점을 가지는 것 같다.
실무에서도 많이 쓰이는 만큼 깊게 공부할 필요가 있다고 느꼈다..