이번 시간에는 프로젝트에서 JPA, Querydsl을 적용한 부분을 정리하고자 한다.
최근 스프링 부트 3.0 이상 버전이 나옴에 따라 바뀐 설정 내용을 함께 공유하려고 한다.
목차는 다음과 같다.
- Gradle 설정
- Jpa Custom Respository 적용
1. Gradle 설정
개발환경은 다음과 같다.
- IntelliJ
- Spring Boot 3.1.1
- Java 17
- Gradle
- Lombok
- DB : MySQL
먼저 build.gradle을 열어 아래와 같이 Querydsl 관련 설정을 추가한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'inf'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//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"
...
}
/**
* QueryDSL Build Options
*/
def querydslDir = "src/main/generated"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}
clean.doLast {
file(querydslDir).deleteDir()
}
tasks.named('test') {
useJUnitPlatform()
}
bootJar {
duplicatesStrategy = 'exclude'
}
여기까지가 querydsl 관련 설정 방법이다.
최근 spring boot 3.0 이상부터 어떻게 사용해야 하는지 정리해보자.
Spring Boot 3.0 이상 사용할 때
위와 같이 Querydsl을 설정하면 QClass 생성 방법이 다르다.
Gradle > Tasks > other > compileJava
를 실행시키면 src/main/generated에 Q 클래스들이 생성된다.
2. Jpa Custom Respository 적용
먼저 사용할 Entity는 다음과 같다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "room")
public class Room {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "room_id")
private Long id;
private Long studyChatBoxId; // 채팅창 pk 저장하는 변수
private String hostEmail; // 방장 닉네임
private String title; // 방 제목
private int expectedUsers; // 인원수 제한
private int studyTimer; // 스터디 타이머
private int currentUserNum; // 현재 인원 수
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL)
private Set<RoomHashTag> roomHashTags = new HashSet<>(); // 방에 여러 태그를 붙일 수 있다.
@Enumerated(EnumType.STRING)
private RoomStatus roomStatus; // 모집 상태 (자리 남았는지? OPEN /CLOSED)
@Enumerated(EnumType.STRING)
private RoomThumbnail thumbnail; // 섬네일 선택지
@JsonIgnore
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<User> participants = new HashSet<>(); // 방 참여자들 (연관 관계)
@Builder(builderMethodName = "createRoom")
public Room(String hostEmail, String title, int expectedUsers, RoomThumbnail thumbnail) {
this.hostEmail = hostEmail;
this.title = title;
this.expectedUsers = expectedUsers;
this.roomStatus = RoomStatus.OPEN;
this.thumbnail = thumbnail;
this.studyTimer = 0;
this.currentUserNum = 0;
}
...
Spring Data Jpa에서는 Custom Repository를 JpaRepository 상속 클래스에서 사용할 수 있도록 기능을 지원한다.
전체적인 그림은 아래와 같다.
위와 같이 구성하면 RoomRepository에서 RoomRepositoryCustomImpl의 코드도 사용할 수 있다.
RoomRepositoryCusom 인터페이스와 RoomRepositoryCusomImpl 클래스를 다음과 작성하면 된다.
RoomRepositoryCusom
package inf.questpartner.repository.room;
import inf.questpartner.controller.dto.RoomSearchCondition;
import inf.questpartner.domain.room.Room;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface RoomRepositoryCustom {
// room 모두 조회 (room - RoomHashTag, User Join해서)
Page<Room> findAllWithTagAndUser(Pageable pageable);
// 특정 Tag가 포함된 Room 모두 조회
Page<Room> findByTagOption(RoomSearchCondition condition, Pageable pageable);
}
RoomRepositoryCusomImpl
package inf.questpartner.repository.room;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import inf.questpartner.controller.dto.RoomSearchCondition;
import inf.questpartner.domain.room.Room;
import inf.questpartner.domain.room.common.tag.TagOption;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import static inf.questpartner.domain.room.QRoom.room;
import static inf.questpartner.domain.room.QRoomHashTag.roomHashTag;
import static inf.questpartner.domain.users.user.QUser.*;
import static org.springframework.util.StringUtils.hasText;
public class RoomRepositoryCustomImpl implements RoomRepositoryCustom {
private final JPAQueryFactory queryFactory;
public RoomRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Page<Room> findAllWithTagAndUser(Pageable pageable) {
QueryResults<Room> result = queryFactory.selectFrom(room)
.leftJoin(room.roomHashTags, roomHashTag).fetchJoin()
.leftJoin(room.participants, user).fetchJoin()
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
return new PageImpl<>(result.getResults(),pageable,result.getTotal());
}
@Override
public Page<Room> findByTagOption(RoomSearchCondition condition, Pageable pageable) {
QueryResults<Room> result = queryFactory.selectFrom(room)
.leftJoin(room.roomHashTags, roomHashTag).fetchJoin()
.leftJoin(room.participants, user).fetchJoin()
.where(tagOptionContains(condition.getTagOption()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
return new PageImpl<>(result.getResults(),pageable,result.getTotal());
}
private static BooleanExpression tagOptionContains(TagOption tagOption) {
return tagOption != null ? roomHashTag.tagOption.eq(tagOption) : null;
}
private static BooleanExpression titleContains(String title) {
return hasText(title) ? room.title.toLowerCase().like("%" + title.toLowerCase() + "%") : null;
}
}
'프로젝트 > 팀프로젝트 qp편' 카테고리의 다른 글
팀 프로젝트 소개 (0) | 2024.05.15 |
---|---|
JPA와 테이블 설계 (0) | 2024.03.22 |
JPA N+1 발생 케이스과 MultipleBagFetchException 해결책 (0) | 2024.03.20 |
관계형 데이터베이스에서의 컬렉션 처리 : Room 엔티티의 구조 개선 (0) | 2024.03.18 |
Stomp를 활용한 실시간 채팅 프로그램 구현 (0) | 2024.03.15 |
댓글