SQL Mapper의 단점
1. 개발자가 직접 SQL을 작성해야 함
SQL을 작성하는 것은 매우 귀찮은 작업일 뿐더러 다음과 같이 테이블의 필드가 하나 추가되면 모든 SQL을 다 뜯어 고쳐야 함
2. 객체와 관계형 데이터베이스의 패러다임의 불일치
상속, 연관관계, 데이터 타입, 데이터 식별 방법 등의 패러다임의 차이를 개발자가 일일이 매핑해줘야 한다.
ORM의 등장과 JPA
SQL Mapper의 단점을 보완하기 위해 등장한 것이 바로 ORM(Object Relational Mapping)
객체는 객체답게 설계하고
테이블은 테이블 답게 설계한다.
중간에서 둘의 차이를 매핑시켜주는 것이 ORM의 역할이다.
JPA
JPA란 Java Persistence API의 약자로 자바진영의 ORM 기술 표준 인터페이스를 의미한다.
JPA의 대표적인 구현체 3가지
- Hibernate
- EclipseLink
- DataNucleus
개발자는 쿼리를 작성할 필요 없이 객체만 전달해주면 JPA가 동적으로 쿼리를 만들어준다.
JPA는 애플리케이션과 JDBC 사이에서 동작한다.
실질적으로 SQL을 날리고 결과를 반환 받는 일은 JDBC가 하지만 JPA가 앞단에서 JDBC를 편리하게 사용할 수 있도록 해준다.
그럼 지금부터 JPA를 사용하는 간단한 방법을 알아보자.
JPA 사용하기
사용법을 이해하기 위한 테스트케이스는 다음과 같다.
1. 아이템 도메인(Item)
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
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;
}
}
2. 아이템 리포지토리(ItemRepository)
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
- Item save(Item item): 아이템 객체를 데이터베이스에 저장
- void update(Long itemId, ItemUpdateDto updateParam): 아이템 객체를 id를 통해 업데이트
- Optional<Item> findById(Long id): id를 통해 아이템 객체 찾기
- List<Item> findAll(ItemSearchCond cond): 특정 조건의 Item 모두찾기
3. 아이템 검색 조건(ItemSearchCond)
@Data
public class ItemSearchCond {
private String itemName;
private Integer maxPrice;
public ItemSearchCond() {
}
public ItemSearchCond(String itemName, Integer maxPrice) {
this.itemName = itemName;
this.maxPrice = maxPrice;
}
}
설정하기
1) build.gradle - 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
참고로 jpa 라이브러리를 추가하면 자동으로 jdbc 라이브러리도 추가된다. 따라서 기존의
implementation 'org.springframework.boot:spring-boot-starter-jdbc'를 추가해놨다면 지워도 무방하다.
2) application.properties - 로그 설정
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
- logging.level.org.hibernate.SQL=DEBUG
하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있음 - logging.level.org.hibernate.orm.jdbc.bind=TRACE
SQL에 바인딩되는 파라미터를 확인할 수 있음
@Entity
JPA에서 객체와 테이블을 매핑하기 위한 어노테이션이다.
객체에 @Entity만 붙여주면 JPA가 인식하고 매핑해준다.
@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;
}
}
- @Id
- 기본키에 해당하는 속성에 붙이는 어노테이션
- @GenertedValue(strategy = GenerationType.IDENTITY)
- 기본키가 데이터베이스에서 생성되는 IDENTITY 전략을 사용할 때 붙이는 어노테이션
- ex: mysql - AUTO_INCREMENT
- @Column
- 데이터베이스의 필드와 매핑할 때 사용하는 어노테이션
- name의 경우 데이터베이스에서 사용되는 필드 명이다.
- length의 경우 해당 필드의 최대 길이이다. (varchar 10)
- 기본생성자
- JPA에서 @Entity를 통해 객체를 매핑할 때 반드시 public 혹은 protected로 된 기본생성자가 있어야 한다.
리포지토리
@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. @Transactional
JPA에서 데이터의 변경(INSERT, UPDATE, DELETE)는 트랜잭션 안에서 일어나야 한다.
조회의 경우에는 상관없다.
2. EntityManager
private final EntityManager em;
public JpaItemRepository(EntityManager em) {
this.em = em;
}
- jpa를 사용하기 위해서는 EntityManager라는 클래스를 사용해야 한다.
- 이 EntityManager는 내부의 데이터소스를 가지고 있어 데이터베이스에 접근할 수 있다.
3. save()
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
- 데이터를 저장할 때는 persist()를 사용한다.
jpa가 생성한 쿼리
insert into item (item_name,price,quantity,id) values (?,?,?,default)
4. findById()
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
- em.find(Item.class, itemId)
- 데이터를 조회시에는 em.find()를 사용한다
- 첫번째 파라미터: 해당 객체의 클래스
- 두번째 파라미터: 기본키
jpa가 생성한 쿼리
select i1_0.id,i1_0.item_name,i1_0.price,i1_0.quantity from item i1_0 where i1_0.id=?
5. update()
@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());
}
- itemId로 Item을 조회한뒤 setter를 통해 데이터를 수정해주었다.
jpa가 생성한 쿼리
update item set item_name=?,price=?,quantity=? where id=?
무언가 이상하다.
분명 위에 코드에선 em.update()같은 코드를 호출하지 않고 단순히 setter를 통해 수정했는데 어떻게 update 쿼리가 실행된 것일까?
JPA는 트랜잭션이 커밋되는 시점에 변경된 엔티티 객체가 있는지 확인해서 특정 엔티티 객체가 변경됐을 경우 Update SQL을 실행한다.
이는 영속성 컨텍스트라는 JPA 내부원리에 의해 발생한다.
6. findAll()
@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();
}
- JPQL
- JPQL이란 JPA에서 제공하는 객체지향 쿼리 언어이다.
- SQL이 테이블을 대상으로 한다면 JPQL은 객체를 대상으로 한다.
- 주로 복잡한 조건으로 조회할 때 사용된다.
- from 뒤에 들어가는 것은 엔티티 객체의 이름이다.
JPQL이 만든 쿼리
select i from Item i where i.itemName like concat('%',:itemName,'%') and i.price <= :maxPrice
JPQL을 통해 실행된 SQL
select
item0_.id as id1_0_,
item0_.item_name as item_nam2_0_,
item0_.price as price3_0_,
item0_.quantity as quantity4_0_
from item item0_
where (item0_.item_name like ('%'||?||'%'))
and item0_.price<=?
정리
1. JPA: 자바 진영 ORM 표준 인터페이스. SQL Mapper의 단점을 극복한 기술. 객체와 테이블 간의 패러다임 불일치를 해결함
2. JPA 사용법
1) @Entity: 테이블에 매핑시킬 객체
2) EntityManager: JPA를 사용하기 위한 클래스
- 저장: persist(객체)
- 조회: find(객체.class, 기본키)
- 변경: setXXX() (JPA의 영속성 컨텍스트에 의해 자동으로 처리됨)
3. JPQL: 객체 중심의 쿼리 언어(SQL은 테이블 중심)
해당 포스트는 김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 기반으로 작성되었습니다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] QueryDSL (0) | 2024.06.07 |
---|---|
[Spring Data] Spring Data JPA (0) | 2024.06.07 |
[Spring Data] MyBatis (0) | 2024.06.06 |
[Spring Data] JdbcTemplate (0) | 2024.06.06 |
[Spring Data] SQL Mapper VS ORM (0) | 2024.06.05 |