목차
- Querydsl 소개
- Querydsl 설정
- Querydsl 적용
Querydsl 소개
Query의 문제점
String sql = "select * from member" +
"where name like ?" +
"and age between ? and ?"
[문자를 합치면]
"select * from memberwhere name like ?and age between ? and ?"
- Query는 문자, Type-check 불가능
- 실행하기 전까지 작동여부 확인 불가
- SQL 문법 오류 발생
에러는 크게 2가지로 분류
- 컴파일 에러(좋은 에러)
- 런타임 에러(나쁜 에러)
Querydsl
- 만약 SQL이 클래스 처럼 타입이 있고 자바 코드로 작성할 수 있다면?
- 쿼리를 자바로 type-safe 하게 개발할 수 있게 지원하는 프레임워크
- 주로 JPA 쿼리(JPQL)에 사용
- Type-safe - 컴파일 시 에러 체크 가능, Code assistant
JPA에서 QUERY 방법은 크게 3가지
- JPQL(HQL)
- Criteria API
- MetaModel Criteria API(type-safe)
1️⃣ JPQL(HQL)
- 장점
- SQL QUERY와 비슷해서 금방 익숙해짐
- 단점
- type-safe 아님
- 동적쿼리 생성이 어려움
2️⃣ Criteria API
- 장점
- 동적 쿼리 생성이 쉬움 ?? (아님)
- 단점
- type-safe가 아님
- 너무 너무 복잡함
- 알아야 할게 너무 많음
3️⃣ MetaModel Criteria API(type-safe)
- root.get("age") ➔ root.get(Member_.age)
- Criteria API + MetaMdoel
- Criteria API와 거의 동일
- type-safe
- 복잡하긴 마찬가지
이런 이유 때문에 이후에 Timowest 라는 사람이 QueryDSL을 만듬
QueryDSL 분석
- Domain(도메인)
- Specific(특화)
- Language(언어)
- 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어
DSL 이란?
- 쿼리 + 도메인 + 특화 + 언어
- 쿼리에 특화된 프로그래밍 언어
- 단순, 간결, 유창
- 다양한 저장소 쿼리 기능 통합
QueryDSL?
- JPA, MongoDB, SQL 같은 기술들을 위해 type-safe SQL을 만드는 프레임 워크
- Type-safe한 Query Type 생성 (코드가 좀 필요하다.)
코드 생성기
- APT: Annotation Processing Tool
- JPA 같은 경우 ➔ @Entity 가 필요
QueryDSL - JPA
- Querydsl은 JPA 쿼리(jpql)을 type-safe 하게 작성하는데 많이 사용됨
- Querydsl - JPA 버전은 Querydsl을 가지고 jpql을 만들어주는 빌더임
- 장점
- type-safe
- 단순하고 쉬운 동적 쿼리 제공
- 감동의 컴파일 에러 제공
- 단점
- Q코드 생성을 위한 APT(코드 생성기)를 설정해야함.
구성
- Query
- ex) from, where, join
- Path
- ex) QMember, QMember.name
- Expression
- ex)name.eq, name.gt
SpringDataJPA + Querydsl
- SpringData 프로젝트의 약점은 조회
- Querydsl로 복잡한 조회 기능 보안
- 단순한 경우: SpringDataJpa
- 복잡한 경우: Querydsl 직접 사용
Querydsl 설정
스프링 부트 2.x와 스프링 부트 3.x의 설정이 다르다.
build.gradle
스프링 부트 2.x 설정
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
build.gradle
스프링 부트 3.x 설정
dependencies {
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
- 2.x 와 비교하면 다음 부분이 jpa ➔ jakarta 로 변경되었다.
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
검증 - Q 타입 생성 확인 방법
1️⃣ 옵션 선택1 - Gradle - Q타입 생성 확인 방법
Gradle IntelliJ 사용법
- Gradle➔ Tasks ➔ build ➔ clean
- Gradle ➔ Tasks ➔ other ➔ compileJava
Gradle 콘솔 사용법
- ./gradlew clean compileJava
Q 타입 생성 확인
- build ➔ generated ➔ sources ➔ annotationProcessor ➔ java/main 하위에
- hello.itemservice.domain.QItem 이 생성되어 있어야 한다.
빌드 파일이 보이지 않는 문제
- 점박이 3개 ➔ Tree Appearance ➔ Show Excluded Files 체크
참고
- Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
- gradle 옵션을 선택하면 Q타입은 gradle build 폴더 아래에 생성되기 때문에 여기를 포함하지 않아야 한다.
- 대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이 부분은 자연스럽게 해결된다.
Q타입 삭제
- gradle clean 을 수행하면 build 폴더 자체가 삭제된다.
- 따라서 별도의 설정은 없어도 된다.
2️⃣ 옵션 선택2 - IntelliJ IDEA - Q타입 생성 확인 방법
- Build ➔ Build Project 또는
- Build ➔ Rebuild 또는
- main() , 또는 테스트를 실행하면 된다.
src/main/generated 하위에 hello.itemservice.domain.QItem 이 생성되어 있어야 한다.
참고
- Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
- IntelliJ IDEA 옵션을 선택하면 Q타입은 src/main/generated 폴더 아래에 생성되기 때문에 여기를 포함하지 않는 것이 좋다.
Q타입 삭제
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
- IntelliJ IDEA 옵션을 선택하면 src/main/generated 에 파일이 생성되고, 필요한 경우 Q파일을 직접 삭제해야 한다.
- gradle 에 해당 스크립트를 추가하면 gradle clean 명령어를 실행할 때 src/main/generated 의 파일도 함께 삭제해준다.
참고
- Querydsl은 이렇게 설정하는 부분이 사용하면서 조금 귀찮은 부분인데, IntelliJ가 버전업 하거나 Querydsl의 Gradle 설정이 버전업 하면서 적용 방법이 조금씩 달라지기도 한다.
- 그리고 본인의 환경에 따라서 잘 동작하지 않기도 한다.
- 공식 메뉴얼에 소개 되어 있는 부분이 아니기 때문에, 설정에 수고로움이 있지만 querydsl gradle 로 검색하면 본인 환경에 맞는 대안을 금방 찾을 수 있을 것이다.
Querydsl 적용
JpaItemRepositoryV3
import static hello.itemservice.domain.QItem.item;
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory query;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.query = new JPAQueryFactory(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(findItem.getPrice());
findItem.setQuantity(findItem.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
public List<Item> findAllOld(ItemSearchCond itemSearch) {
String itemName = itemSearch.getItemName();
Integer maxPrice = itemSearch.getMaxPrice();
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(itemName)) {
builder.and(item.itemName.like("%" + itemName + "%"));
}
if (maxPrice != null) {
builder.and(item.price.loe(maxPrice));
}
List<Item> result = query
.select(item)
.from(item)
.where(builder)
.fetch();
return result;
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
// Querydsl 사용
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
// QItem item = new QItem("i");
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
return result;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
공통
- Querydsl을 사용하려면 JPAQueryFactory 가 필요하다.
- JPAQueryFactory 는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager 가 필요하다.
- 설정 방식은 JdbcTemplate 을 설정하는 것과 유사하다.
- 참고로 JPAQueryFactory 를 스프링 빈으로 등록해서 사용해도 된다.
save(), update(), findById()
- 기본 기능들은 JPA가 제공하는 기본 기능을 사용한다.
findAllOld
- Querydsl을 사용해서 동적 쿼리 문제를 해결한다.
- BooleanBuilder 를 사용해서 원하는 where 조건들을 넣어주면 된다.
- 이 모든 것을 자바 코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있다.
findAll
앞서 findAllOld 에서 작성한 코드를 깔끔하게 리팩토링 했다.
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
- Querydsl에서 where(A,B) 에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다.
- 참고로 where() 에 null 을 입력하면 해당 조건은 무시한다.
- 이 코드의 또 다른 장점은 likeItemName() , maxPrice() 를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다.
- 쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 있는 큰 장점이다.
QuerydslConfig
@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
private final EntityManager em;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
ItemServiceApplication - 변경
//@Import(SpringDataJpaConfig.class)
@Import(QuerydslConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
테스트 실행
@Slf4j
@SpringBootTest
@Transactional
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
/*@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach() {
// 트랜잭션 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}*/
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
// 트랜잭션 롤백
// transactionManager.rollback(status);
}
@Test
void save() {
//given
Item item = new Item("itemA", 10000, 10);
//when
Item savedItem = itemRepository.save(item);
//then
Item findItem = itemRepository.findById(item.getId()).get();
log.info("itemId: {}", item.getId());
assertThat(findItem).isEqualTo(savedItem);
}
@Test
void updateItem() {
//given
Item item = new Item("item1", 10000, 10);
Item savedItem = itemRepository.save(item);
Long itemId = savedItem.getId();
//when
ItemUpdateDto updateParam = new ItemUpdateDto("item2", 20000, 30);
itemRepository.update(itemId, updateParam);
//then
Item findItem = itemRepository.findById(itemId).get();
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
@Test
void findItems() {
//given
Item item1 = new Item("itemA-1", 10000, 10);
Item item2 = new Item("itemA-2", 20000, 20);
Item item3 = new Item("itemB-1", 30000, 30);
log.info("repository={}", itemRepository.getClass()); // 예외 변환을 해주는 Proxy
itemRepository.save(item1);
itemRepository.save(item2);
itemRepository.save(item3);
//둘 다 없음 검증
test(null, null, item1, item2, item3);
test("", null, item1, item2, item3);
//itemName 검증
test("itemA", null, item1, item2);
test("temA", null, item1, item2);
test("itemB", null, item3);
//maxPrice 검증
test(null, 10000, item1);
//둘 다 있음 검증
test("itemA", 10000, item1);
}
void test(String itemName, Integer maxPrice, Item... items) {
List<Item> result = itemRepository.findAll(new ItemSearchCond(itemName, maxPrice));
assertThat(result).containsExactly(items);
}
}
정리
Querydsl 덕분에 동적 쿼리를 매우 깔끔하게 사용할 수 있다.
쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
- 메서드 추출을 통해서 코드를 재사용할 수 있다.
- 예를 들어서 여기서 만든 likeItemName(itemName) , maxPrice(maxPrice) 메서드를 다른 쿼리에서도 함께 사용할 수 있다.
'Spring' 카테고리의 다른 글
[Spring DB] 스프링 트랜잭션 전파 (0) | 2024.09.03 |
---|---|
[Spring DB] 스프링 트랜잭션 (2) | 2024.09.02 |
[Spring DB] 데이터 접근 기술 - 스프링 데이터 JPA (1) | 2024.08.30 |
[Spring DB] 데이터 접근 기술 - JPA (0) | 2024.08.29 |
[Spring DB] 데이터 접근 기술 - MyBatis (2) | 2024.08.28 |