JPA

QueryDSL

iksadnorth 2023. 9. 6. 00:21

해당 게시물은 책 '자바 ORM 표준 프로그래밍'을 읽고 작성했습니다.

👣 개요

QueryDSL은 JPQL 언어를 그대로 작성함으로서 일어나는 단점을 커버하기 위해 생긴 기술이다.
해당 기술을 통해 런타임이 아닌 컴파일 단계에서 오류를 미리 잡아낼 수 있다는 특징이 있고
동적으로 쿼리를 작성할 수 있다는 장점이 있다.

 

👣 의존성 추가

Gradle 기준으로 QueryDSL의 의존성 추가하는 방법이다.

dependencies {
    ...

    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    // MySQL
    implementation 'mysql:mysql-connector-java:8.0.28'
}

// QClass 경로 정의
def querydslDir = "$buildDir/generated/querydsl"

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile).configureEach {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

// gradle clean 시에 QClass 디렉토리 삭제
clean.doLast {
    file(querydslDir).deleteDir()
}

...

인터넷에 많이 퍼진 방법 중에 플러그인을 사용하는 방법이 있는데 해당 방법은
./gradlew build 명령어로 빌드를 하면 빌드 실패가 뜨는 버그가 존재하므로 
위와 같은 방법으로 설정해야 한다.

 

[Spring] QueryDsl gradle 설정 (Spring boot 3.0 이상)

스프링 부트 3.0이상에서의 Querydsl 설정방법

velog.io

 

 

QueryDsl SpringBoot 2.7의 gradle 설정을 공유합니다. - 인프런 | 고민있어요

plugins { id 'org.springframework.boot' version '2.7.4' id 'io.spring.dependency-management' version '1.0.14.RELEASE' id 'java' } group = 'study' ve...

www.inflearn.com

 

👣 QClass

QClass는 QueryDSL을 사용하는 이유라고 볼 수 있는 빌드 결과물이다.
@Entity로 매핑한 클래스의 필드명을 필드로 가진 QClass를 빌드 과정으로 자동으로 생성한 뒤,
해당 클래스로 쿼리를 작성하게 된다.

@Entity @Table(name = "order")
@Getter @Setter @NoArgsConstructor
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Member member;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Item item;
}
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QOrder extends EntityPathBase<Order> {

    private static final long serialVersionUID = 1321720944L;

    private static final PathInits INITS = PathInits.DIRECT2;

    public static final QOrder order = new QOrder("order1");

    public final DateTimePath<java.time.LocalDateTime> createdAt = createDateTime("createdAt", java.time.LocalDateTime.class);

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final QItem item;

    public final QMember member;

    public QOrder(String variable) {
        this(Order.class, forVariable(variable), INITS);
    }

    public QOrder(Path<? extends Order> path) {
        this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
    }

    public QOrder(PathMetadata metadata) {
        this(metadata, PathInits.getFor(metadata, INITS));
    }

    public QOrder(PathMetadata metadata, PathInits inits) {
        this(Order.class, metadata, inits);
    }

    public QOrder(Class<? extends Order> type, PathMetadata metadata, PathInits inits) {
        super(type, metadata, inits);
        this.item = inits.isInitialized("item") ? new QItem(forProperty("item")) : null;
        this.member = inits.isInitialized("member") ? new QMember(forProperty("member")) : null;
    }

}

아래 코드는 QClass를 초기화하는 방법이다.
생성자에 String을 주면 해당 엔티티의 별칭을 지정할 수 있다.

QItem itemSub = new QItem("itemSub"); // 직접 별칭 지정
QItem itemSub = QItem.item; // 기본 별칭 지정

// select itemSub from Item as itemSub;

 

👣 검색 조건 쿼리

where 절 내부에 사용되는 메서드에 대한 코드다.

@Entity @Table(name = "order")
@Getter @Setter @NoArgsConstructor
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Member member;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Item item;
}
// StringPath name = order.item.name;
name.eq("member1") // username = 'member1'   
name.ne("member1") //username != 'member1'  
name.eq("member1").not() // username != 'member1'
name.isNotNull()  
name.like("member%")
name.contains("member")
name.startsWith("member")

// NumberPath<Long> colId = order.id;
colId.in(10, 20)
colId.notIn(10, 20) 
colId.between(10,30)
colId.goe(30) // age >= 30 
colId.gt(30) // age > 30   
colId.loe(30) // age <= 30 
colId.lt(30) // age < 30

// DateTimePath<LocalDateTime> colCreatedAt = order.createdAt;
colCreatedAt.after(LocalDateTime.now());
colCreatedAt.before(LocalDateTime.now());
colCreatedAt.between(LocalDateTime.MIN, LocalDateTime.MAX);
colCreatedAt.dayOfYear();
colCreatedAt.dayOfMonth();
colCreatedAt.dayOfWeek();

 

👣 페이징, 정렬

query.from(order)
        .orderBy(order.createdAt.desc(), order.id.asc())
        .offset(10).limit(20)
        .fetch();

QueryModifiers modifiers = new QueryModifiers(20L, 10L);
query.from(order)
        .orderBy(order.createdAt.desc(), order.id.asc())
        .restrict(modifiers)
        .fetch();

 

👣 Group By & Having

query.from(order)
        .groupBy(order.createdAt)
        .having(order.count().lt(5))
        .fetch();

 

👣 Join

// inner join
query.from(order)
        .innerJoin(order.member)
        .fetch();
// left outer join
query.from(order)
        .leftJoin(order.item)
        .fetch();
// fetch join
query.from(order)
        .join(order.member).fetchJoin()
        .fetch();
// on 절 사용
query.from(order)
        .join(order.member)
        .on(member.count().gt(2))
        .fetch();
// cross join
query.from(order, item)
        .where(order.item.eq(item))
        .fetch();

 

👣 서브 쿼리

JPAQuery<Item> itemJPAQuery = new JPAQuery<>();
QItem itemSub = new QItem("itemSub");
List<Item> itemsHalfOfMaxPrice = itemJPAQuery.from(item)
        .where(item.price.eq(
                new JPAQuery<>()
                        .select(itemSub.price.max().multiply(0.5))
                        .from(itemSub).fetchOne()
        )).fetch();

 

👣 프로젝션

List<Tuple> list = query.select(order.id, order.createdAt).from(order)
        .join(order.member).fetchJoin()
        .where(order.item.name.eq("떡볶이"))
        .fetch();

아래와 같이 DTO를 만들어서 해당 형식으로 담아낼 수도 있다.

public class OrderDateDto {
    public Long id;
    public String createdAt;

    public OrderDateDto(Long id, String createdAt) {
        this.id = id;
        this.createdAt = createdAt;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setCreatedAt(String createdAt) {
        this.createdAt = createdAt;
    }
}
List<OrderDateDto> dtoList = query.select(
    Projections.bean(
            OrderDateDto.class,
            order.id.as("id"),
            order.createdAt.as("createdAt")
    )
)
.from(order)
.join(order.member).fetchJoin()
.where(order.item.name.eq("떡볶이"))
.fetch();

DTO에 주입할 때, 다양한 방법으로 주입할 수 있다.
1. 필드 주입.
2. Setter 주입.
3. 생성자 주입.

Projections.bean( // Setter를 사용해서 주입
        OrderDateDto.class,
        order.id.as("id"),
        order.createdAt.as("createdAt")
);
Projections.fields( // 필드를 이용해서 주입
        OrderDateDto.class,
        order.id.as("id"),
        order.createdAt.as("createdAt")
);
Projections.constructor( // 생성자를 이용해서 주입
        OrderDateDto.class,
        order.id.as("id"),
        order.createdAt.as("createdAt")
);

 

👣 Distinct

List<Order> ordersDistinct = query.distinct().from(item)
        .fetch();

 

👣 삽입, 수정, 삭제 배치 쿼리

// 생성
JPAInsertClause insertClause = new JPAInsertClause(em, item);
insertClause.set(item.price, 100L)
        .execute();

// 수정
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
updateClause.where(item.name.eq("칫솔"))
        .set(item.price, item.price.add(100))
        .execute();

// 삭제
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
deleteClause.where(item.name.eq("칫솔"))
        .execute();

        JPAQuery<Item> itemQuery = new JPAQuery<>();
        itemQuery.from(item)
                .where(item.price.eq(1L));

 

👣 메서드 위임

메서드 위임이란? QClass의 메서드를 사용자가 직접 정의할 수는 없다.
하지만 QueryDSL은 해당 메서드를 생성할 수 있는 방법을 마련해뒀다.

방법은 아래와 같다.

1. 정적 메서드로 생성하고 싶은 QClass의 메서드를 생성한다.
단, 첫번째 파라미터는 무조건 해당 기능을 적용할 엔티티 QClass여야 한다.

public class ItemExpression {
    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem item, Integer price) {
        return item.price.gt(price);
    }

    @QueryDelegate(Item.class)
    public static NumberExpression<Long> addOne(QItem item) {
        return item.price.add(1L);
    }
}

2. 빌드를 수행해 해당 메서드가 적용됐는지 확인

3. 해당 기능을 사용하기

// addOne 메서드 사용.
query.from(order)
        .where(order.item.addOne().gt(10L))
        .fetch();

// isExpensive 사용.
query.from(order)
        .where(order.item.isExpensive(1000))
        .fetch();

'JPA' 카테고리의 다른 글

12장 Spring Data JPA  (0) 2023.09.06
영속성 컨텍스트 vs JPQL  (0) 2023.09.06
10장 객체지향 쿼리 언어  (0) 2023.09.04
9장 값 타입  (0) 2023.09.03
8장 프록시와 연관관계 관리  (0) 2023.08.31