본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 방명록 미니 프로젝트] 2-1. 프로젝트 생성과 Querydsl

반응형

1. 프로젝트의 계층적 구조와 객체 구성

2. Querydsl을 이용해 동적쿼리 전달해서 검색 조건 처리

3. Entity 객체와 DTO 구분

4. 화면에서 페이징 처리

 


1. 화면설계서

  • 목록
    • 전체 목록 페이징 처리해 조회
    • 제목/내용/작성자 항목으로 검색과 페이징 처리
  • 등록
    • 새로운 글 등록 후 다시 목록 화면으로 이동
  • 조회
    • 목록 화면에서 특정 글 선택시 자동으로 조회 화면으로 이동
    • 수정/삭제가 가능한 화면으로 이동가능
  • 수정/삭제
    • 수정 화면에서는 삭제가 가능하고, 삭제 후에는 목록 페이지로 이동
    • 글 수정 후에는 다시 조회 화면으로 이동해 수정 내용 확인 가능

 

 

PRG 패턴 : post-redirect-get

-> 수행한 기능을 새로고침 할 경우, 다시 수행하지 않도록 리다이렉션하는 패턴

 


 

컨트롤러에서 URL 처리해야하는 메서드들

@RequestMapping

@GetMapping

@PostMapping

기능 URL GET/POST 기능 Redirect URL
목록 /guestbook/list GET 목록/페이징/검색  
등록 /guestbook/register GET 입력 화면  
  /guestbook/register POST 등록 처리 /guestbook/list
조회 /guestbook/read GET 조회 화면  
수정 /guestbook/modify GET 수정/삭제 가능 화면  
  /guestbook/modify POST 수정 처리 /guestbook/read
삭제 /guestbook/remove POST 삭제 처리 /guestbook/list

 

 

프로젝트 기본 구조

 

1. URL의 요청은 컨트롤러에서 처리
2. 컨트롤러는 Service 타입에 주입받는 구조, 컨트롤러가 서비스를 호출해 서비스에서 처리
-> 서비스는 인터페이스로 구현하는 클래스가 실제로 작업 처리를 리포지토리에 요청
3. DB에 접근하는건 JPA가 리포지토리에서 대신한다. SQL 대신 스프링 데이터 JPA를 이용해 CRUD 메서드 처리
4. 리포지토리에서 반환받은 결과는 타임리프를 이용해 뷰에 전달

 

 

 

 

DTO는 각 계층 사이에서 데이터를 주고 받는다.

-> Entity와 존재 목적이 다른 이유

1. URL에서 전달된 요청은 컨트롤러에서 DTO 형태로 처리
2. 리포지토리는 엔티티 타입을 이용하기 때문에 서비스 계층에서 DTO와 엔티티 변환을 처리한다.

즉, DTO와 함께 메서드 요청을 받으면 서비스는 DTO를 엔티티로 변환해 리포지토리에 전달
리포지토리가 해당 메서드를 DB에 처리한 후 엔티티를 반환하는데, 서비스는 엔티티를 DTO로 변환해 컨트롤러로 전달
컨트롤러는 뷰에 DTO를 Model로 전달하는데, 뷰에서는 타임리프를 이용해 전달받은 엔티티에 접근한다.
-> 따라서 엔티티는 항상 영속상태로 존재하는 것이 좋다.

프로젝트 생성

 

1. Spring Data JPA, Oracle Driver, Lombok, Spring WEB, Spring DevTools, ThymeLeaf 의존성 선택

 

2. application.properties에 DB연동과 JPA 설정

#DataSource Setting
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521/xe
spring.datasource.username=system
spring.datasource.password=pass

#JPA Setting
spring.jpa.hibernate.ddl-auto=update
# -> 프로젝트 실행시 자동으로 DDL 생성
spring.jpa.properties.hibernate.format_sql=true;
# -> SQL 포맷팅해서 출력
spring.jpa.show-sql=true
# -> JPA 처리시 발생하는 SQL 출력


#Thymeleaf에서 변경 후에 만들어진 결과를 보관하지 않도록 설정
#또한, 뷰의 수정 후에 자동으로 결과 반영이 이루어지도록 Update classes and resources 설정

 

4. pom.xml

<!-- 오라클 JDBC 드라이버 -->
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 타임리프 날짜시간의존성-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

 

5. 컨트롤러 생성

 

@Controller
@RequestMapping("/guestbook")
@Log4j2
public class GuestbookController {
    @GetMapping({"/", "/list"})
    public String list(){

        log.info("list..........");

        return "/guestbook/list";
    }

 

6. 리스트 페이지 작성

: 타임리프의 레이아웃을 이용해 fragment의 content 부분만 작성해서 반환

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!-- fragment 가져오기-->
<th:block th:replace="~{/layout/basic :: setContent(~{this.content})}">
          <th:block th:fragment="content">
            <h1>Guest Book List Page</h1>
          </th:block>
</th:block>
</html>
더보기

부트스트랩에서 가져온, sidebar

https://startbootstrap.com/template/simple-sidebar

 

Simple Sidebar - Bootstrap Sidebar Template - Start Bootstrap

Like our free products? Our pro products are even better! Go Pro Today!

startbootstrap.com

-> 해당 자료에서 static부분을 다운로드해 프로젝트에 삽입한다.

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">


<th:block th:fragment="setContent(content)">

    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>

<body>

<style>
    * {
        margin: 0;
        padding: 0;
    }
    .header {
        width:100vw;
        height: 20vh;
        background-color: aqua;
    }
    .content {
        width: 100vw;
        height: 70vh;
        background-color: lightgray;
    }
    .footer {
        width: 100vw;
        height: 10vh;
        background-color: green;
    }
</style>


<div class="header">
    <h1>HEADER</h1>
</div>
<div class="content" >

    <th:block th:replace = "${content}">
    </th:block>

</div>

<div class="footer">
    <h1>FOOTER</h1>
</div>

</body>
</th:block>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!-- 부트스트랩에서 가져온 사이드바 이용-->
<!-- 해당 위치를 가리키는 ~{}-->
<!-- basic에 전달하는 파라미터로 content를 전송한다. 그 후 해당 basic으로 대체 즉, 여기 존재하는 content를 전달하고 basic에 넣어서 반환-->
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">


    <th:block th:fragment="content">

        <h1>exSidebar Page</h1>

    </th:block>

</th:block>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!-- 부트스트랩 이용-->

<!-- 해당 파일에서 정의하는 컨텐츠 부분-->
<th:block th:fragment="setContent(content)">
    <head>

<head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Simple Sidebar - Start Bootstrap Template</title>

    <title>Simple Sidebar - Start Bootstrap Template</title>

    <!-- 타임리프를 이용한 링크 전달-->
    <!-- link th: href="@{static부터 시작되는 절대경로}" rel="사용할 이름"> -->

    <!-- Bootstrap core CSS -->
    <link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link th:href="@{/css/simple-sidebar.css}" rel="stylesheet">

    <!-- Bootstrap core JavaScript -->
    <script th:src="@{/vendor/jquery/jquery.min.js}"></script>
    <script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>

</head>

<body>

<div class="d-flex" id="wrapper">

    <!-- Sidebar -->
    <div class="bg-light border-right" id="sidebar-wrapper">
        <div class="sidebar-heading">Start Bootstrap </div>
        <div class="list-group list-group-flush">
            <a href="#" class="list-group-item list-group-item-action bg-light">Dashboard</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Shortcuts</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Overview</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Events</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Profile</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Status</a>
        </div>
    </div>
    <!-- /#sidebar-wrapper -->

    <!-- Page Content -->
    <div id="page-content-wrapper">

        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <button class="btn btn-primary" id="menu-toggle">Toggle Menu</button>

            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ml-auto mt-2 mt-lg-0">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            Dropdown
                        </a>
                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
            </div>
        </nav>

        <div class="container-fluid">

            <th:block th:replace = "${content}"></th:block>

        </div>

    </div>
    <!-- /#page-content-wrapper -->

</div>
<!-- /#wrapper -->




<!-- Menu Toggle Script -->
<script>
    $("#menu-toggle").click(function(e) {
        e.preventDefault();
        $("#wrapper").toggleClass("toggled");
    });
</script>

</body>
</th:block>
</html>

 

 

 

7. 리스너를 이용해 엔티티 객체의 생성/변경 감지

: AuditingEntityListener를 통해 regDate, modDate에 값 대입

JPA의 엔티티 생성 시간, 수정 시간을 사용하는 리스너

 

 

리스너 사용 조건 : 리스너 클래스와 메인 클래스에 필요한 어노테이션들

 

(1) 리스너 클래스

@EntityListeners(value = {AuditingEntityListener.class})
    @CreatedDate
    @LastModifiedDate
//실제 엔티티 생성 시간이 아니라, 요청 받은 시간이어야 하기 때문에
//@CreatedDate의 경우 컬럼에 updatable = false 설정 필요

(2) 메인 클래스

@SpringBootApplication
//리스너 사용을 위한 설정
@EnableJpaAuditing
public class Boot2Application {

    public static void main(String[] args) {
        SpringApplication.run(Boot2Application.class, args);
    }

}

 

 

8. 공통부분을 작성한 BaseEntity를 상속해서 클래스 생성

 

package com.boot2.guestbook.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

//공통부분을 정의한 클래스 -> 객체로만 존재하는 가상 테이블
@MappedSuperclass

//리스너 : 언제 어떤 사용자가 삭제 요청했는지 로그로 남기는 이벤트 처리
//-> 엔티티 생명주기에 따른 이벤트 처리 가능하므로, 모든 기록을 리스너 하나로 처리 가능
//PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후, 또는 refresh를 호출한 후(2차 캐시에 저장되어 있어도 호출된다.)
//PrePersist : persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우에는 엔티티의 식별자는 존재하지 않는 상태이다. 새로운 인스턴스를 merge 할 때도 수행된다.
//PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
//PreRemove : remove 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval 에 대해서는 flush나 commit 시에 호출된다.
//PostPrsist : flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY인 경우 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로, 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
//PostUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.(persist 시에는 호출되지 않는다)
//PostRemove : flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.

//적용 가능 위치
//1. 엔티티에 직접 적용
//2. 별도의 리스너 등록
//3. 기본 리스너 사용

//-> 댓글 작성 시간, 수정 시간 값 대입을 위해 리스너 사용
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
public class BaseEntity {


    //엔티티 생성시간 : @CreatedDate
    //-> 디비 반영할 때 값 변경을 막기위해 updatable=false로 지정
    @CreatedDate
    @Column(name="regdate", updatable = false)
    private LocalDateTime regDate;

    //엔티티 최종 수정 시간 : @LastModifiedDate
    @LastModifiedDate
    @Column(name="moddate")
    private LocalDateTime modDate;
}

-> @Getter 필요

 

 

package com.boot2.guestbook.entity;

import lombok.*;

import javax.persistence.*;

//인자가 모두 있을 때의 생성자와, 기본 생성자 자동 생성
//toString() 메서드 자동생성
//빌더 패턴 사용
@Entity
@Getter @Setter
@AllArgsConstructor @NoArgsConstructor
@ToString
@Builder
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를 만들지 않는 걸 권장.

-> 메서드를 통한 변경만 허용한다.

 

 

9. 동적 쿼리 처리를 위한 Querydsl를 적용해, GuestbookRepository 작성

: 쿼리메서드와 @Query를 이용한 JPQL로는 선언시 고정된 형태의 값만 가지는 단점을 해결

-> 복잡한 검색조건, 조인, 서브쿼리 기능도 구현이 가능

 

 

<주의> 빌드 도구에 따라, 방법이 다릅니다.

(1) Querydsl 사용을 위한 작업

 

의존성 추가

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

플러그인 추가

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>src/main/generated</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

 

(2) .gitignore

### Querydsl###
generated

 

-> main 아래에 generated 폴더가 생성되기 때문에

 

(3) 인터페이스인 CustomRepository 생성 후, 구현 클래스 작성

package com.boot2.guestbook.repository;

public interface CustomRepository {
}

 

package com.boot2.guestbook.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomRepositoryImplRepository implements CustomRepository {
    private final JPAQueryFactory jpaQueryFactory;

}

 

10. Querydsl을 이용한 사용자 정의 리포지토리 생성

-> CustomRepository 인터페이스도 함께 상속

package com.boot2.guestbook.repository;

import com.boot2.guestbook.entity.Guestbook;
import com.querydsl.core.BooleanBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

//Querydsl를 이용한 사용자 리포티조티에는 QuerydslPredicateExecutor<Guestbook> 인터페이스도 함께 상속한다.
public interface GuestbookRepository extends JpaRepository<Guestbook, Long>, CustomRepository , QuerydslPredicateExecutor<Guestbook> {

}

 

11. Querydsl를 이용한 엔티티 작성 테스트

package com.boot2.guestbook.repository;

import com.boot2.guestbook.entity.Guestbook;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class GestbookRepositoryTests {

    @Autowired
    private GuestbookRepository guestbookRepository;

    @Test
    public void insertDummies(){
        IntStream.rangeClosed(1,300).forEach(i->{

            Guestbook guestbook = Guestbook.builder()
                    .title("Title...."+i)
                    .content("Content...."+i)
                    .writer("user"+(i%10))
                    .build();
            System.out.println(guestbookRepository.save(guestbook));
        });
    }
}

12. Querydsl을 이용한 엔티티 검색 테스트

-> Querydsl를 이용하면, 단일 항목 검색, 다중 항목 검색 모두 가능하다.

 

Querydsl 사용법

1. 동적 처리를 위한 Q도메인 클래스 import -> Maven update
Q도메인 클래스를 이용해 엔티티 클래스에 선언된 필드를 변수로 활용

2. BooleanBuilder를 이용해 where문에 들어가는 조건들을 담는다.

3. 필드 값을 결합해 원하는 조건 생성
단, BooleanBuilder에 전달하는 인자는 querydsl.core.types.Predicate타입 이어야 한다.

4. 만들어진 조건은 where문에 and, or 그리고 between 키워드와 결합

5. BooleanBuilder를 이용해 리포지토리에 추가된 QuerydslPredicateExecutor 인터페이스의 findAll() 메서드 사용 가능

-> 페이지 처리와 동시에 검색 처리

 

Querydsl 검색 조건

(1) 단일 항목 검색 테스트 (1개의 항목)

(2) 다중 항목 검색 테스트 (2개의 항목)

(3) 다중 항목 검색 테스트 (3개의 항목)

 

 

package com.boot2.guestbook.repository;

import com.boot2.guestbook.entity.Guestbook;
import com.boot2.guestbook.entity.QGuestbook;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.stream.IntStream;

//import : Boolean관련은 querydsl page관련은 data.domain
@SpringBootTest
public class GuestbookGuestbookRepositoryTests {

    @Autowired
    private GuestbookRepository guestbookRepository;

    @Test
    //방명록 작성 테스트
    public void insertDummies(){
        //기본형 int스트림을 이용해 정수를 통한 구간 범위 데이터 흐름 만들기
        //빌드 패턴을 이용해 객체 생성
        IntStream.rangeClosed(1,300).forEach(i -> {

            Guestbook guestbook = Guestbook.builder()
                    .title("Title...." + i)
                    .content("Content..." +i)
                    .writer("user" + (i % 10))
                    .build();
            System.out.println(guestbookRepository.save(guestbook));
        });
    }

    @Test
    //방명록 수정 테스트
    public void updateTest( ){
        //존재하는 번호로 테스트
        Optional<Guestbook> result = guestbookRepository.findById(300L);

        //해당 값이 존재한다면 -> null이 아니라면
        if(result.isPresent()){
            Guestbook guestbook=result.get();
            guestbook.changeTitle("Changed Title.....");
            guestbook.changeContent("Changed Content.....");

            guestbookRepository.save(guestbook);
        }

    }
    //Querydsl을 복잡한 검색 구현
    //사용법
    //1. BooleanBuilder 생성
    //2. 조건에 맞을 경우 Predicate 타입 함수를 생성
    //3. BooleanBuilder에 작성된 Predicate 추가하고 실행

    //구현
    //1. 단일 항목 테스트 : 제목/내용/작성자와 같이 단 하나의 항목으로 검색
    //2. 다중 항목 테스트 (1): 제목+내용, 내용+작성자, 제목+작성자와 같이 2개 항목으로 검색
    //3. 다중 항목 테스트 (2): 제목+내용+작성자와 같이 3개 항목으로 검색

    //1. 단일 항목 테스트

    @Test
    public void testQuery1() {

        Pageable pageable = PageRequest.of(0, 10, Sort.by("gno").descending());

        QGuestbook qGuestbook = QGuestbook.guestbook; //1

        String keyword = "1";

        BooleanBuilder builder = new BooleanBuilder();  //2

        BooleanExpression expression = qGuestbook.title.contains(keyword); //3

        builder.and(expression); //4

        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable); //5

        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });

    }

    @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); // 1----------------

        builder.and(exAll); //2-----

        builder.and(qGuestbook.gno.gt(0L)); // 3-----------

        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);

        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });

    }

    @Test
    public void testQuery3() {

        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 exWriter = qGuestbook.writer.contains(keyword);

        BooleanExpression exAll = exTitle.or(exContent).or(exWriter); // 1----------------

        builder.and(exAll); //2-----

        builder.and(qGuestbook.gno.gt(0L)); // 3-----------

        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);

        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });

    }
        //1. 동적 처리를 위한 Q도메인 클래스 import -> Maven update
        //Q도메인 클래스를 이용해 엔티티 클래스에 선언된 필드를 변수로 활용

        //2. BooleanBuilder를 이용해 where문에 들어가는 조건들을 담는다.

        //3. 필드 값을 결합해 원하는 조건 생성
        //단, BooleanBuilder에 전달하는 인자는 querydsl.core.types.Predicate타입 이어야 한다.

        //4. 만들어진 조건은 where문에 and, or 그리고 between 키워드와 결합

        //5. BooleanBuilder를 이용해 리포지토리에 추가된 QuerydslPredicateExecutor 인터페이스의 findAll() 메서드 사용 가능

        //-> 페이지 처리와 동시에 검색 처리



}
반응형