👣 개요
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 명령어로 빌드를 하면 빌드 실패가 뜨는 버그가 존재하므로
위와 같은 방법으로 설정해야 한다.
👣 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 |