Querydsl

2024. 2. 6. 17:28
728x90

- JPQL의 단점 : 문자열의 형태로 작성하여 정상적으로 작성되었는지는 실행해봐야 알 수 있다. 

복잡한 쿼리를 작성하기 어렵고, 상황에 따라 조건을 달리해서 조회하는 동적 쿼리를 만들기 어렵다. 

 

1. Querydsl

- 목표 : 데이터를 조회하는 여러 맥락에 대하여 같은 Java 코드로 데이터를 조회하는 것을 목표로 한다. (JPA, SQL, Lucene(검색 엔진), Mongodb(NoSQL DB) 등)

>> 실행 중(런타임)에만 오류를 확인할 수 있다는 JPQL의 단점이 해소된다.

http://querydsl.com/

 

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();

이 테이블을 기준으로 엔티티를 만들었다. 

- 엔티티 참고

더보기
// 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();

 

728x90

'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

BELATED ARTICLES

more