Querydsl
- JPQL의 단점 : 문자열의 형태로 작성하여 정상적으로 작성되었는지는 실행해봐야 알 수 있다.
복잡한 쿼리를 작성하기 어렵고, 상황에 따라 조건을 달리해서 조회하는 동적 쿼리를 만들기 어렵다.
1. Querydsl
- 목표 : 데이터를 조회하는 여러 맥락에 대하여 같은 Java 코드로 데이터를 조회하는 것을 목표로 한다. (JPA, SQL, Lucene(검색 엔진), Mongodb(NoSQL DB) 등)
>> 실행 중(런타임)에만 오류를 확인할 수 있다는 JPQL의 단점이 해소된다.
Querydsl - Unified Queries for Java
Unified Queries for Java. Querydsl is compact, safe and easy to learn. <!-- Querydsl Unified Queries for Java Querydsl provides a unified querying layer for multiple backends in Java. Compared to the alternatives Querydsl is more compact, safer and easier
querydsl.com
ex) JPQL vs. Querydsl
- JPQL EntityManager로 단순한 조회 쿼리 : 문자열에서 오타를 발견하기 어렵다.
Item found = entityManager.createQuery("""
select i from Item i where i.name = :name
""", Item.class)
.setParameter("name", "itemA")
.getSingleResult();
- Querydsl 방식으로 조회 쿼리 : Java 코드이기 때문에 오타나 오류가 발생하면 컴파일 단계에서 먼저 검증 가능!
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QItem qitem = new QItem("item");
Item found = queryFactory.selectFrom(qitem)
.where(qitem.name.eq("itemA"))
.fetchOne();
![](https://blog.kakaocdn.net/dn/eHOVHp/btsEnZ3G9fX/WPsRQFy2sT51qz6fkdqB8K/img.png)
이 테이블을 기준으로 엔티티를 만들었다.
- 엔티티 참고
// BaseEntity
@Getter
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
// Item Entity
@Getter
@Setter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Item extends BaseEntity {
private String name;
private String description;
private Integer price;
private Integer stock;
@ManyToOne
private Shop shop;
}
// Shop Entity
@Getter
@Setter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Shop extends BaseEntity{
private String name;
private String description;
}
// JpaConfig
@Configuration
@EnableJpaAuditing
@RequiredArgsConstructor
public class JpaConfig {}
1) 기본 설정
- 의존성 추가 (build.gradle)
// build.gradle
dependencies {
//querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
- build하기 : 우측 코끼리 Gradle에서 build 더블클릭 후 빌드하기
- 확인하기 : 프로젝트 파일에서 build > generated > sources > annotationProcessor > java > main 내부에 Q<Entity> 파일들이 생성된다.
2. Querydsl 사용해보기
1) JPAQueryFactory
: Querydsl과 JPA를 사용할 때 사용하는 객체. (Query(JPQL)를 만들기 위한 빌더 역할)
-(1) EntityManager를 사용하는 방식이 정의된 클래스가 JPAQueryFactory
-(2) 혹은 JPAQueryFactory를 빈 객체로 등록 >> @Configuration에서 가능. (어디서든 Bean 객체로 주입 가능!)
// (1) @Repository의 생성자에서 등록
@Slf4j
@Repository
public class QueryDslRepo {
// query를 만들기 위한 builder 역할
private final JPAQueryFactory queryFactory;
public QueryDslRepo(EntityManager entityManager) {
// queryFactory 생성할 때 EntityManager 빈 객체를 전해주면 된다.
queryFactory = new JPAQueryFactory(entityManager);
}
}
// (2) @Configuration에서 Bean 객체로 등록
// JpaConfig class
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
// QueryDslRepo class
@Slf4j
@Repository
@RequiredArgsConstructor
public class QueryDslRepo {
private final JPAQueryFactory jpaQueryFactory;
}
2) QType
: Querydsl에서 자동으로 생성해주는 클래스.
- 만들었던 엔티티의 속성들이 같은이름, 다른 자료형으로 정의되어 있다.
>> 이름 자체를 문자열로 전달하는 대신, QType의 속성으로 전달하여 타입 안정성이 높다.
3) 간단한 쿼리 만들어보기.
- JPAQueryFactory에는 SQL 작성시 사용하는 절들이 메서드로 정의되어 있다 >> 그래서 빌더 형태로 SQL을 작성 가능.
@Slf4j
@Repository
@RequiredArgsConstructor
public class QueryDslRepo {
// query를 만들기 위한 builder 역할
private final JPAQueryFactory jpaQueryFactory;
private final ItemRepository itemRepository;
public void helloQuerydsl() {
// new item 저장
itemRepository.save(Item.builder()
.name("new item")
.price(1000)
.stock(1000).build());
// QItem 타입 객체 생성
QItem qItem = new QItem("item");
// SQL문에 QItem 타입의 엔티티를 전달하며 엔티티를 반환받는다.
List<Item> items = jpaQueryFactory.select(qItem) // SELECT i
.from(qItem) // FROM Item i
.fetch();
for (Item item : items) {
log.info("{}: {} ({})", item.getName(), item.getPrice(), item.getStock());
}
}
}
그럼 테스트 코드를 통해 메서드들을 하나씩 살펴보자.
3. 기본적인 데이터 조회
1) 테스트 코드 클래스 + beforeEach 코드 추가
// QuerydslQTypeTests test class
@Transactional
@SpringBootTest
@ActiveProfiles("test")
public class QuerydslQTypeTests {
@Autowired
private ItemRepository itemRepository;
@Autowired
private ShopRepository shopRepository;
@Autowired
private JPAQueryFactory jpaQueryFactory;
// @BeforeEach: 각 테스트 전에 실행할 코드를 작성하는 영역
// shops와 items를 저장한다.
@BeforeEach
public void beforeEach() {
// shopA 생성 후 저장
Shop shopA = shopRepository.save(Shop.builder()
.name("shopA")
.description("shop A description")
.build());
// shopB 생성 후 저장
Shop shopB = shopRepository.save(Shop.builder()
.name("shopB")
.description("shop B description")
.build());
// itemA, itemB, itemC, itemD, itemE, null 아이템 생성 후 저장.
itemRepository.saveAll(List.of(
Item.builder()
.shop(shopA)
.name("itemA")
.price(5000)
.stock(20)
.build(),
Item.builder()
.shop(shopA)
.name("itemB")
.price(6000)
.stock(30)
.build(),
Item.builder()
.shop(shopB)
.name("itemC")
.price(8000)
.stock(40)
.build(),
Item.builder()
.shop(shopB)
.name("itemD")
.price(10000)
.stock(50)
.build(),
Item.builder()
.name("itemE")
.price(11000)
.stock(10)
.build(),
Item.builder()
.price(10500)
.stock(25)
.build()
));
}
}
2) 메서드 살펴보기
ex)
@Transactional
@SpringBootTest
@ActiveProfiles("test")
public class QuerydslQTypeTests {
@Autowired
private ItemRepository itemRepository;
@Autowired
private ShopRepository shopRepository;
@Autowired
private JPAQueryFactory queryFactory;
@Test
public void qType() {
QItem qItem1 = new QItem("item1");
Item found = queryFactory.select(qItem1) //SELECT item1
.from(qItem1) //FROM Item item1
.where(qItem1.name.eq("itemA")) //WHERE item1.name = "itemA";
.fetchOne();
assertEquals(found.getName(), "itemA");
QItem qItem2 = new QItem("item2");
found = jpaQueryFactory
.selectFrom(qItem2) //SELECT item2 FROM Item item2
.where(qItem2.name.eq("itemB")) WHERE item2.name = "itemB"
.fetchOne();
assertEquals(found.getName(), "itemB");
}
}
- select() : SELECT절
- selectFrom() : select와 from의 대상이 동일하다면 사용. (SELECT * FROM)
- new QItem("item")에서의 "item" : 쿼리에서 사용할 Item의 별칭(Alias) 지정.
** 별칭을 사용하면 같은 QType 인스턴스라고 해도 별칭이 다르면 명확하게 구분하여 사용해야 한다.
실제로 자기자신을 대상으로 연관관계가 있을 때 두 엔티티를 구분하기 위해 만들곤 한다. (ex. User 엔티티의 팔로잉, 팔로워 등 자기 자신을 참조할 때 구분하기 위해 사용)
- 필요하지 않을 때에는 QType 내부의 정적 QType 인스턴스를 사용한다.
(1) QItem.item : QType 클래스명.변수명으로 불러오기
(2) item : import static을 이용해 가져와 변수명으로 불러오기
// 1. QType 클래스명.변수이름 으로 불러오기
Item found = queryFactory
// 기본 제공 정적 QItem
.selectFrom(QItem.item)
.where(QItem.item.name.eq("itemA"))
.fetchOne();
// 2. static 메서드를 import해서 변수이름만으로 불러오기
import static com.example.querydsl.entity.QItem.item;
// ...
public void test() {
// static import를 하면 클래스명을 건너뛸 수 있다.
Item found = queryFactory
.selectFrom(item)
.where(item.name.eq("itemB"))
.fetchOne();
}
// ...
3) fetch()
: 결과를 조회할 때 fetch로 시작하는 여러 메서드 활용 가능.
- fetch() : 결과 전체 리스트 조회
// fetch() 단순하게 전체 조회
List<Item> foundList = jpaQueryFactory.selectFrom(item)
.fetch(); //결과를 리스트 형태로 조회
assertEquals(6, foundList.size());
- fetchOne() : 단일 결과 조회
- 없을시 null
- 결과값이 2개 이상일 시 예외 발생
// 1개 조회
Item found = jpaQueryFactory
.selectFrom(item)
.where(item.id.eq(1L))
.fetchOne(); //하나만 조회
assertEquals(1L, found.getId());
found = jpaQueryFactory.selectFrom(item)
.where(item.id.eq(0L))
.fetchOne(); //없을 경우 null이 반환된다.
assertNull(found);
assertThrows(Exception.class, () ->
// 2개 이상인 경우 Exception 발생.
jpaQueryFactory.selectFrom(item).fetchOne());
- fetchFirst() : 결과 중 첫번째 조회 (= LIMIT 1)
// fetchFirst() 첫번째 결과 또는 null
found = jpaQueryFactory.selectFrom(item)
.fetchFirst(); //LIMIT 1 -> fetchOne()
assertNotNull(found);
+ 추가적으로 offset()과 limit() 설정 가능
// offset, limit 설정
foundList = jpaQueryFactory
.selectFrom(item)
.offset(3)
.limit(2)
.fetch();
for (Item foundItem : foundList) {
System.out.println(foundItem.getId());
}
+ Deprecated 메서드 (향후 미지원...) : fetchCount() (결과의 갯수 반환), fetchResults() (결과와 관련된 여러 정보 반환)
// fetchCount() : 결과의 갯수 반환 (deprecated)
long count = jpaQueryFactory.selectFrom(item)
.fetchCount();
assertEquals(6, count);
//fetchResults() : 결과 및 count+offset+limit 정보 반환 (deprecated)
QueryResults<Item> results = jpaQueryFactory.selectFrom(item)
.offset(3)
.limit(2)
.fetchResults();
System.out.println(results.getTotal());
System.out.println(results.getOffset());
System.out.println(results.getLimit());
System.out.println(results.getResults()); //실제 내용은 getResults()
4) orderBy() 정렬
- orderBy() : QType을 이용해 어떤 속성을 어떤 순서로 정렬할지 작성. 넣은 순서대로 작동
List<Item> foundList = queryFactory.selectFrom(item)
// item.(속성).(순서)를 일반적인 ORDER BY 절에 넣듯 순서대로
.orderBy(
// ASC도 명시할 것
item.price.asc(),
item.stock.desc()
)
.fetch();
- null값의 순서는 어떻게 할지도 결정 가능. nullLast() / nullFirst()
List<Item> foundList = queryFactory.selectFrom(item)
// item.(속성).(순서)를 일반적인 ORDER BY 절에 넣듯 순서대로
.orderBy(
// ASC도 명시할 것
item.price.asc(),
item.stock.desc(),
item.name.asc().nullsLast()
// item.name.asc().nullsFirst()
)
.fetch();
5) where() 조건절
: 조건을 전달하는 기준도 QType을 기준으로 진행.
equals | = | eq | item.name.eq("itemA"); |
not equals | != | ne | item.name.ne("itemB"); |
not (not equals) | !(!=) | .not() : not()이 붙으면 조건의 반대 | item.name.ne("itemC").not(); |
is null | isNull() | item.name.isNull(); | |
is not null | isNotNull() | item.name.isNotNull(); | |
str.length() !=0 | isNotEmpty() | item.name.isNotEmpty(); | |
less than | < | lt | item.price(lt(6000); |
less or equal | <= | loe |
item.price(loe(6000);
|
greater or equal | >= | goe | item.price(goe(6000); |
greater than | > | gt | item.price(gt(6000); |
like | % | like() : like는 SQL문법을 따름 | item.name.like("%item_"); |
contains | %_% | contains() | item.name.contains("item"); |
startsWith | args% | startsWith() | item.name.startsWith("item"); |
endsWith | %args | endsWith() | item.name.endsWith("A"); |
between | between(a,b) | item.price.between(2000, 4000); | |
in | in(a,b,c,d) | item.price.in(3000, 4000, 6000); | |
after | after() | item.createdAt.after(LocalDateTime.now().minusDays(5)); 지금으로부터 5일전 이후 |
|
before | before() | item.createdAt.before(LocalDateTime.now().minusDays(5)); 지금으로부터 5일전 이전 |
- 하나의 조건 전달 / 각 조건에 대하여 and()나 or() 메서드로 여러 조건을 연쇄할 수 있다. 혹은 ,컴마로 인자로 넣어 조건을 나열할 수 있다.
List<Item> foundItems = queryFactory.selectFrom(item)
// and, or 메서드로 조건 연쇄 가능
// 한번 사용할 때마다 현재 조건 (and | or) 인자 조건
.where(item.name.startsWith("item")
.and(item.price.lt(6000))
.or(item.price.goe(7000))
)
.fetch();
List<Item> foundItems = jpaQueryFactory
.selectFrom(item)
// where에 복수개 넣어주면, 전부 만족 (AND로 엮임)
.where(
item.name.isNotNull(),
item.price.lt(8000),
item.stock.gt(20)
)
.fetch();
'Programming > Spring, SpringBoot' 카테고리의 다른 글
[JPA] N+1 (0) | 2024.02.05 |
---|---|
낙관적 락 & 비관적 락 (1) | 2024.02.05 |
[JPA] 영속성 컨텍스트 (Persistence Context) (0) | 2024.02.05 |
@Query (1) | 2024.02.01 |
Relations (M : N 관계) (1) | 2024.02.01 |