동적 쿼리 작성을 위해 Querydsl을 추가합니다.
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'org.zerock'
version = '0.0.1-SNAPSHOT'
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-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation('org.springframework.boot:spring-boot-starter-test')
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.1.0'
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"
}
tasks.named('test') {
useJUnitPlatform()
}
def generated = 'src/main/generated'
// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [ generated ]
}
// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}
위에 build.gradle은 다른 자료(JPA강의나 현재 교재)에 적혀있는 내용과는 많이 달라졌습니다. Spring Boot가 버전 3.0으로 업그레이드됨에 따라 @Entity
가 담겨있는 클래스 등이 javax.persistence에서 jakarta.persistence로 import문이 완전히 변경되었기 때문입니다.
https://stackoverflow.com/questions/60021815/why-has-javax-persistence-api-been-replaced-by-jakarta-persistence-api-in-spring
오라클의 JAVA 저작권때문에 이름이 변경된 것인데, 이것때문에 javax.persistence.entity를 찾지 못하는 에러가 발생했고, 스프링부트의 버전업이 비교적 최근이라 자료도 많이 나오지않아 찾는데 정말 오래걸렸습니다.
java.lang.NoClassDefFoundError: javax/persistence/Entity 에러
해당 코드 수정은 인프런 김영한님 강의 커뮤니티에서 QueryDsl SpringBoot 3.0의 gradle 설정을 공유합니다와 querydsl 설정 후 테스트 코드 실행 시 QHello 에러를 참고하여 진행하였습니다.
정상적으로 수행이 완료되면 아래와 같이 generated 폴더에 Querydsl에 맞는 엔티티가 생성됩니다.
아래 사진과 같이 Querydsl의 엔티티에는 모든 애트리뷰트값이 변수명으로 선언되어 있음을 확인할 수 있습니다.
GuestBookRepository에 QuerydslPredicateExecutor<GuestBook>
을 추가로 상속합니다.
public interface GuestbookRepository extends JpaRepository<Guestbook, Long>, QuerydslPredicateExecutor<Guestbook> {
}
다음으론 Guestbook 엔티티에 제목과 내용을 수정하는 코드를 입력하겠습니다.
package org.zerock.guestbook.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gno;
@Column(length=100, nullable = false)
private String title;
@Column(length=1500, nullable = false)
private String content;
@Column(length=50, nullable = false)
private String writer;
//추가
public void changeTitle(String title) {
this.title = title;
}
public void changeContent(String content) {
this.content = content;
}
}
엔티티는 setter가 없는 방향으로 개발하는 것이 권장되지만, 연습을 위해 추가하겠습니다.
@Test
public void updateTest() {
Optional<Guestbook> result = guestbookRepository.findById(300L);
if(result.isPresent()) {
Guestbook guestbook = result.get();
guestbook.changeTitle("Changed Title...");
guestbook.changeContent("Changed Content...");
guestbookRepository.save(guestbook);
}
}
위의 테스트를 실행시키고 데이터베이스를 확인해보면 300번째 엔티티가 수정되었음을 확인할 수 있습니다. 또한 modDate역시 엔티티 수정을 자동으로 감지하여 수정해주기 때문에 regDate와 다름을 확인할 수 있습니다.
//Pageable을 import할 때 print가 아닌 data.domain을 import해야 합니다.
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
<생략>
@Test
public void testQuery1() {
Pageable pageable = PageRequest.of(0, 10, Sort.by("gno").descending());
//엔티티 클래스의 필드를 변수로 사용가능하므로 컴파일 시 쿼리문 검사가능
QGuestbook qGuestbook = QGuestbook.guestbook;
String keyword = "1";
//쿼리문의 Where절에 들어갈 조건을 넣는 컨테이너
BooleanBuilder builder = new BooleanBuilder();
//BooleanBuilder엔 com.querydsl.core.types.Predicate 타입만 들어갈 수 있음
BooleanExpression expression = qGuestbook.title.contains(keyword);
//Where절의 And구문
builder.and(expression);
//레포지토리가 상속받는 QuerydslPredicateExecutor 인터페이스의 findAll을 통해 where절 탐색
Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);
result.stream().forEach(guestbook -> {
System.out.println(guestbook);
});
}
쿼리문의 Where절을 보면 정상적으로 질의처리를 하고 있음을 확인할 수 있습니다.
그리고 결과의 첫 10개(1페이지)가 출력되는 것 역시 확인할 수 있습니다.
@Test
public void testQuery2() {
Pageable pageable = PageRequest.of(0, 10, Sort.by("gno").descending());
QGuestbook qGuestbook = QGuestbook.guestbook;
String keyword = "1";
BooleanBuilder builder = new BooleanBuilder();
BooleanExpression exTitle = qGuestbook.title.contains(keyword);
BooleanExpression exContent = qGuestbook.content.contains(keyword);
BooleanExpression exAll = exTitle.or(exContent);
builder.and(exAll);
builder.and(qGuestbook.gno.gt(0L));
Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);
result.stream().forEach(guestbook -> {
System.out.println(guestbook);
});
}
위와 같이 where절에서 and와 or로 적절한 질의처리가 진행되고 있음을 볼 수 있습니다.