안녕하세요. J4J입니다.
이번 포스팅은 JPA에서 직접 쿼리를 작성할 수 있게 해주는 @Query에 대해 적어보는 시간을 가져보려고 합니다.
JPQL
JPA가 쿼리를 자동으로 생성해주지만 상황에 따라 직접 쿼리를 작성할 필요가 생기게 됩니다.
JPA에서 직접 쿼리를 작성할 수 있는 방법은 다음과 같이 2가지가 있습니다.
- JPQL로 작성
- 일반 SQL로 작성
JPQL이라고 하는 것은 JPA의 일부분으로 정의된 플랫폼 독립적인 객체지향 쿼리 언어입니다.
JPA에서 사용할 수 있는 쿼리 언어로 일반 SQL이 데이터베이스를 바라보고 작성한다면 JPQL은 엔티티 클래스를 바라보고 작성해야 합니다.
JPQL과 SQL모두 직접 쿼리를 작성하는 방법은 동일하게 @Query어노테이션을 이용하면 됩니다.
그리고 @Query어노테이션에 들어있는 nativeQuery라는 속성을 이용하여 JPQL로 작성한 것인지 SQL로 작성한 것인지를 구분할 수 있습니다.
- nativeQuery = true → SQL
- nativeQuery = false (default) → JPQL
또한 메서드 명은 기존 자동생성 방식과 달리 자유롭게 작성할 수 있습니다.
관련 코드를 생성하여 어떻게 작성할 수 있는지를 보여드리도록 하겠습니다.
기본 세팅
먼저 테이블, 엔티티 생성 등의 기본 세팅을 해주도록 하겠습니다.
※ JPA 환경 설정은 여기를 참고하세요.
주제는 과자로 하겠습니다.
[ 1. MySQL에 테이블 / 데이터 생성 ]
create table snack (
snack_id int auto_increment,
name varchar(50),
price int,
primary key(snack_id)
);
insert into snack(name, price) values ('허니버터칩', 1500);
insert into snack(name, price) values ('스윙칩', 2000);
insert into snack(name, price) values ('롤리폴리', 1200);
insert into snack(name, price) values ('포스틱', 1600);
insert into snack(name, price) values ('감자깡', 1600);
[ 2. Snack 엔티티 클래스 ]
package com.spring.jpa.dto;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "snack")
public class Snack implements Serializable {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "snack_id")
private int id;
private String name;
private int price;
}
[ 3. SnackRepository 클래스 ]
package com.spring.jpa.repository;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.spring.jpa.dto.Snack;
public interface SnackRepository extends JpaRepository<Snack, Integer>{ // 제네릭 타입: <엔티티 클래스, 엔티티클래스의 기본키>
}
[ 4. 단위 테스트 ]
package com.spring.jpa;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.jpa.config.RootContext;
import com.spring.jpa.dto.Snack;
import com.spring.jpa.repository.SnackRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
@Autowired
SnackRepository snackRepository;
@Test
public void snackTest() {
}
}
@Query 사용 방법 (일반 쿼리)
- Repository
package com.spring.jpa.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.spring.jpa.dto.Snack;
public interface SnackRepository extends JpaRepository<Snack, Integer>{ // 제네릭 타입: <엔티티 클래스, 엔티티클래스의 기본키>
// 일반 JPQL쿼리, from뒤는 엔티티 명 (소문자로 할 시 에러)
@Query(value = "select sn from Snack sn")
public List<Snack> selectAllJPQL();
// 일반 SQL쿼리
@Query(value = "select snack_id, name, price from snack", nativeQuery = true)
public List<Snack> selectAllSQL();
}
- 단위 테스트
package com.spring.jpa;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.jpa.config.RootContext;
import com.spring.jpa.dto.Snack;
import com.spring.jpa.repository.SnackRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
@Autowired
SnackRepository snackRepository;
@Test
public void snackTest() {
log.info(snackRepository.selectAllJPQL().toString());
log.info(snackRepository.selectAllSQL().toString());
}
}
- 실행 결과 로그
Hibernate:
/* select
sn
from
Snack sn */ select
snack0_.snack_id as snack_id1_1_,
snack0_.name as name2_1_,
snack0_.price as price3_1_
from
snack snack0_
INFO : com.spring.jpa.JPATest - [Snack(id=1, name=허니버터칩, price=1500), Snack(id=2, name=스윙칩, price=2000), Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* dynamic native SQL query */ select
snack_id,
name,
price
from
snack
INFO : com.spring.jpa.JPATest - [Snack(id=1, name=허니버터칩, price=1500), Snack(id=2, name=스윙칩, price=2000), Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
@Query 사용 방법 (파라미터 사용)
- Repository
package com.spring.jpa.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.spring.jpa.dto.Snack;
public interface SnackRepository extends JpaRepository<Snack, Integer>{ // 제네릭 타입: <엔티티 클래스, 엔티티클래스의 기본키>
// JPQL은 엔티티의 변수명이 id이기 때문에 조건절에 sn.id
// JPQL 일반 파라미터 쿼리, @Param 사용 X
@Query(value = "select sn from Snack sn where sn.id > ?1")
public List<Snack> selectJPQLById1(int id);
// JPQL 일반 파라미터 쿼리, @Param 사용 O
@Query(value = "select sn from Snack sn where sn.id > :id")
public List<Snack> selectJPQLById2(@Param(value = "id") int id);
// JPQL 객체 파라미터 쿼리
@Query(value = "select sn from Snack sn where sn.id > :#{#paramSnack.id}")
public List<Snack> selectJPQLById3(@Param(value = "paramSnack") Snack snack);
// SQL은 테이블의 컬럼명이 snack_id이기 때문에 조건절에 snack_id
// SQL 일반 파라미터 쿼리, @Param 사용 X
@Query(value = "select snack_id, name, price from snack where snack_id > ?1", nativeQuery = true)
public List<Snack> selectSQLById1(int id);
// SQL 일반 파라미터 쿼리, @Param 사용 O
@Query(value = "select snack_id, name, price from snack where snack_id > :id", nativeQuery = true)
public List<Snack> selectSQLById2(@Param(value = "id") int id);
// SQL 객체 파라미터 쿼리
@Query(value = "select snack_id, name, price from snack where snack_id > :#{#paramSnack.id}", nativeQuery = true)
public List<Snack> selectSQLById3(@Param(value = "paramSnack") Snack snack);
}
- 단위 테스트
package com.spring.jpa;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.jpa.config.RootContext;
import com.spring.jpa.dto.Snack;
import com.spring.jpa.repository.SnackRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
@Autowired
SnackRepository snackRepository;
@Test
public void snackTest() {
log.info(snackRepository.selectJPQLById1(2).toString());
log.info(snackRepository.selectJPQLById2(2).toString());
log.info(snackRepository.selectJPQLById3(new Snack(2, null, 0)).toString());
log.info(snackRepository.selectSQLById1(2).toString());
log.info(snackRepository.selectSQLById2(2).toString());
log.info(snackRepository.selectSQLById3(new Snack(2, null, 0)).toString());
}
}
- 실행 결과 로그
Hibernate:
/* select
sn
from
Snack sn
where
sn.id > ?1 */ select
snack0_.snack_id as snack_id1_1_,
snack0_.name as name2_1_,
snack0_.price as price3_1_
from
snack snack0_
where
snack0_.snack_id>?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* select
sn
from
Snack sn
where
sn.id > :id */ select
snack0_.snack_id as snack_id1_1_,
snack0_.name as name2_1_,
snack0_.price as price3_1_
from
snack snack0_
where
snack0_.snack_id>?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* select
sn
from
Snack sn
where
sn.id > :__$synthetic$__1 */ select
snack0_.snack_id as snack_id1_1_,
snack0_.name as name2_1_,
snack0_.price as price3_1_
from
snack snack0_
where
snack0_.snack_id>?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* dynamic native SQL query */ select
snack_id,
name,
price
from
snack
where
snack_id > ?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* dynamic native SQL query */ select
snack_id,
name,
price
from
snack
where
snack_id > ?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
Hibernate:
/* dynamic native SQL query */ select
snack_id,
name,
price
from
snack
where
snack_id > ?
INFO : com.spring.jpa.JPATest - [Snack(id=3, name=롤리폴리, price=1200), Snack(id=4, name=포스틱, price=1600), Snack(id=5, name=감자깡, price=1600)]
@Modifying / @Transactional
위의 쿼리처럼 select구문이 아닌 DML(insert, update, delete) 구문을 사용할 때는 다음의 어노테이션을 추가적으로 사용해야 됩니다.
- @Modifying
- @Transactional
먼저 @Modifying은 insert, update, delete 뿐만 아니라 DDL구문을 사용할 때도 표기를 해줘야 됩니다.
왜냐하면 영속성 컨텍스트에 오래된 데이터를 비워주고 새로운 데이터를 읽어오기 위해서입니다.
그리고 @Transactional은 update, delete를 할 때 표기를 해줘야 정상 실행이 됩니다.
update구문으로 예제 코드를 만들어 보겠습니다.
- Repository
package com.spring.jpa.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import com.spring.jpa.dto.Snack;
public interface SnackRepository extends JpaRepository<Snack, Integer>{ // 제네릭 타입: <엔티티 클래스, 엔티티클래스의 기본키>
// JPQL은 엔티티의 변수명이 id이기 때문에 조건절에 sn.id
// JPQL 쿼리
@Query(value = "update Snack set name = :#{#paramSnack.name}, price = :#{#paramSnack.price} where id = :#{#paramSnack.id}")
@Modifying
@Transactional
public int updateJPQL(@Param(value = "paramSnack") Snack snack);
// SQL은 테이블의 컬럼명이 snack_id이기 때문에 조건절에 snack_id
// SQL 쿼리
@Query(value = "update snack set name = :#{#paramSnack.name}, price = :#{#paramSnack.price} where snack_id = :#{#paramSnack.id}", nativeQuery = true)
@Modifying
@Transactional
public int updateSQL(@Param(value = "paramSnack") Snack snack);
}
- 단위 테스트
package com.spring.jpa;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.jpa.config.RootContext;
import com.spring.jpa.dto.Snack;
import com.spring.jpa.repository.SnackRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
@Autowired
SnackRepository snackRepository;
@Test
public void snackTest() {
Snack snack = snackRepository.findById(3).get();
snack.setName("메로나");
snack.setPrice(1000);
snackRepository.updateJPQL(snack);
log.info(snackRepository.findById(3).toString());
snack.setName("츄파츕스");
snack.setPrice(500);
snackRepository.updateSQL(snack);
log.info(snackRepository.findById(3).toString());
}
}
- 실행 결과 로그
Hibernate:
select
snack0_.snack_id as snack_id1_1_0_,
snack0_.name as name2_1_0_,
snack0_.price as price3_1_0_
from
snack snack0_
where
snack0_.snack_id=?
Hibernate:
/* update
Snack
set
name = :__$synthetic$__1,
price = :__$synthetic$__2
where
id = :__$synthetic$__3 */ update
snack
set
name=?,
price=?
where
snack_id=?
Hibernate:
select
snack0_.snack_id as snack_id1_1_0_,
snack0_.name as name2_1_0_,
snack0_.price as price3_1_0_
from
snack snack0_
where
snack0_.snack_id=?
INFO : com.spring.jpa.JPATest - Optional[Snack(id=3, name=메로나, price=1000)]
Hibernate:
/* dynamic native SQL query */ update
snack
set
name = ?,
price = ?
where
snack_id = ?
Hibernate:
select
snack0_.snack_id as snack_id1_1_0_,
snack0_.name as name2_1_0_,
snack0_.price as price3_1_0_
from
snack snack0_
where
snack0_.snack_id=?
INFO : com.spring.jpa.JPATest - Optional[Snack(id=3, name=츄파츕스, price=500)]
참조
[스프링 데이터 JPA] 4-6. 스프링 데이터 JPA: Update 쿼리 메소드
이상으로 JPA에서 직접 쿼리를 작성할 수 있게 해주는 @Query에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > JPA' 카테고리의 다른 글
[JPA] MyBatis와 동시 사용 (DTO/엔티티 통합, 연관관계 매핑) (0) | 2021.03.31 |
---|---|
[JPA] MyBatis와 동시 사용 (DTO/엔티티 분리) (7) | 2021.03.30 |
[JPA] 연관관계 매핑 (양방향) (0) | 2021.03.28 |
[JPA] 연관관계 매핑 (단방향) (0) | 2021.03.26 |
[JPA] 복합키(Composite Key) 엔티티 (2) | 2021.03.24 |
댓글