jnk1m
Foliage IT
jnk1m
전체 방문자
오늘
어제
  • 분류 전체보기 (209)
    • Today I Learned (34)
    • Java (47)
    • Database (15)
    • [NHN Academy] (27)
    • Spring (47)
    • HTML + CSS + JavaScript (11)
    • JSP (3)
    • Node.js (10)
    • React Native (2)
    • 기타 (8)
    • 스크랩 (5)

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
글쓰기 / 관리자
jnk1m

Foliage IT

Spring

[SpringBoot] 2022/05/30 OneToMany

2022. 6. 4. 00:46

**One To Many
1.연관 관계 와 관계형 데이터베이스
=>관계형 데이터베이스에서는 1:1, 1:N(N:1), M:N 의 관계를 이용해서 데이터가 서로 간에 어떤 관계를 가지고 있는지 표현 
이 때 Primary Key 와 Foreign Key를 이용해서 관계를 설정
=>JPA에서는 어노테이션 과 방향성을 이용해서 표현하게 되는데 데이터베이스에서는 방향성의 개념이 없습니다.

1:1 -> @OneToOne
1:N -> @OneToMany
N:1 -> @ManyToOne
N:M -> @ManyToMany

=>방향성은 단방향 과 양방향이 있습니다.
  
1)1:1 관계
=>한쪽 테이블의 하나의 행과 다른쪽 테이블의 하나의 행이 매칭이 되는 경우
=>이 경우는 양쪽 테이블의 기본키를 다른 테이블에 외래키로 추가해주어야 합니다.
=>JPA 에서는 양쪽의 기본키에 @OneToOne을 설정하면 자동으로 추가를 해줍니다.

2)1:N 관계
=>회원 과 게시글의 관계
한 명의 회원은 여러 개의 게시글을 작성할 수 있고 하나의 게시글은 한 명의 회원이 작성해야 합니다.
관계형 데이터베이스에서는 회원의 기본키를 게시글의 외래키로 추가해야 합니다.
JPA에서는 회원의 기본키에 @OneToMany를 설정해도 되고 게시글 쪽의 외래키에 @ManyToOne을 설정해도 됩니다.

3)M:N 관계
=>회원 과 상품의 관계
한 명의 회원은 여러 개의 다른 상품을 구매할 수 있습니다.
동일한 상품을 여러 명의 회원이 구매할 수 있습니다.
데이터베이스에서는 별도의 테이블을 만들어서 각 테이블의 기본키를 새로 만든 테이블의 외래키로 추가합니다.
JPA에서는 @ManyToMany로 설정은 가능하지만 @OneToMany 나 @ManyToOne 2개로 분할해서 설정

4)회원, 게시글, 댓글 의 관계
회원 과 게시글은 1:N
게시글 과 댓글은 1:N
회원 과 댓글은 1:N

2.board 애플리케이션 생성
1)프로젝트 생성
=>의존성:Spring Boot Devtools, Lombok, Spring Web, Thymeleaf, Spring Data JPA, MySQL

2)build.gradle 파일에 시간 처리 관련 의존성을 추가
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time'

3)application.properties 파일에 기본 설정을 추가
#server 의 port 설정
server.port = 80

#연결할 데이터베이스 설정 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/adam?useUnicode=yes&characterEncoding=UTF-8&serverTimezon=UTC
spring.datasource.username=adam
spring.datasource.password=wnddkd

#연결할 데이터베이스 설정 - Oracle
#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@192.168.10.4:1521:xe
#spring.datasource.username=system
#spring.datasource.password=oracle

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
logging.level.org.hibernate.type.descriptor.sql=trace

spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false

4)EntryPoint 클래스에 데이터베이스 감시 설정 추가
@EnableJpaAuditing

5)기본이 되는 속성들을 갖는 model.BaseEntity 생성
//공통된 속성을 가진 Entity

//테이블로 생성할 필요가 없음
@MappedSuperclass
//데이터베이스 작업을 감시
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
//abstract: 인스턴스를 생성할 수 없도록 해주는 클래스로 상속을 통해서만 사용이 가능
abstract public class BaseEntity {
//생성한 시간을 저장하는데 컬럼 이름은 regdate 이고 수정할 수 없도록 생성
@CreatedDate
@Column(name="regdate", updatable=false)
private LocalDateTime regDate;

//수정한 시간을 저장하는데 컬럼 이름은 moddate 이고 수정할 수 없도록 생성
@LastModifiedDate
@Column(name="moddate", updatable=false)
private LocalDateTime modDate;
}

3.Model 생성
1)회원 정보를 저장할 Entity 생성 - model.Member
=>email(기본키), password, name
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;


@Entity
@Table(name = "tbl_member")

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Member extends BaseEntity {
@Id
private String email;
private String password;
private String name;
}

2)게시물 정보를 저장할 Board Entity 생성 - model.Board
=>게시글번호(기본키 자동 생성), 제목, 내용
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Board extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long bno;
private String title;
private String content;
}

3)댓글 정보를 저장할 Reply Entity를 생성 - model.Reply
=>댓글 번호, 댓글 내용, 댓글 작성자
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long rno;
private String text;
private String replyer;
}

4.관계 어노테이션의 세부 속성
1)mappedBy: 양방향 연관 관계를 만들 때 매핑되는 Entity의 필드를 연결

2)cascade: Entity 의 상태 변화를 전파시키는 옵션
=>PERSIST - 부모 Entity 가 저장될 때 자식 Entity도 같이 저장
=>MERGE - 부모 Entity 가 병합될 때 자식 Entity도 같이 병합
=>REMOVE - 부모 Entity 가 삭제될 때 자식 Entity도 같이 삭제
=>REFRESH - 부모 Entity 가 refresh 될 때 자식 Entity도 같이 refresh
=>DETACH - 부모 Entity 가 detach 될 때 자식 Entity도 같이 detach
=>ALL - 부모 Entity의 모든 변화가 자식 Entity에 전파

3)orphanRemoval
=>고아 객체를 삭제해주는 옵션
=>Primary Key 값이 NULL 인 데이터가 고아 객체(orphanRemoval): Entity 는 데이터베이스에 있었던 데이터이지만 실제로는 메모리에 존재하는 데이터

4)fetch
=>관련있는 Entity를 가져오는 시점을 설정하는 것인데 Eagar(바로 가져오기) 와 Lazy(지연)가 있음

5.ManyToOne 어노테이션
=>Board 테이블 과 Member 테이블은 Board 쪽에서 바라보면 N:1 의 관계입니다.
객체 지향에서 이런 관계를 표현하고자 할 때는 Board 쪽에 Member를 참조할 수 있는 속성을 하나 추가하거나 Member 쪽에 Board를 포함할 수 있는 List 나 배열 같은 속성을 추가합니다.

관계형 데이터베이스에서는 Member 테이블의 기본키를 Board 테이블에 외래키로 설정합니다.

=>Spring JPA에서는 속성 위에 @OneToMany 나 @ManyToOne을 이용해서 설정을 합니다.

1)Board Entity에 Member Entity를 참조할 수 있는 속성을 추가
//Member Entity를 N:1 관계로 참조
@ManyToOne
private Member member;

2)Reply Entity에 Board Entity를 참조할 수 있는 속성을 추가
@ManyToOne
private Board board;

3)프로젝트 실행

4)데이터베이스 확인
desc tbl_member;

desc board;

desc reply;

=>board 테이블에는 member_기본키 컬럼이 추가되어 있고 reply 테이블에는 board_기본키 컬럼이 추가되어 있어야 합니다.

5.Repository 생성
1)Member Entity를 위한 Repository 인터페이스 생성 - persistence.MemberRepository
public interface MemberRepository extends JpaRepository<Member, String>{

}

2)Board Entity를 위한 Repository 인터페이스 생성 - persistence.BoardRepository
public interface BoardRepository extends JpaRepository<Board, Long>{

}

3)Reply Entity를 위한 Repository 인터페이스 생성 - persistence.ReplyRepository
public interface ReplyRepository extends JpaRepository<Reply, Long>{

}

6.Repository Test
1)Repository 의 작업을 Test 하기 위한 클래스를 src/test/java 디렉토리의 패키지에 생성 - RepositoryTest
@SpringBootTest
public class RepositoryTest {

}

2)Member 테이블에 100 개의 데이터를 삽입
@Autowired
private MemberRepository memberRepository;

@Test
public void insertMembers() {
IntStream.rangeClosed(1, 100).forEach(i -> {
Member member = Member.builder().email("ggangpae" + i + "@aaa.com").password("1111")
.name("사용자" + i).build();
memberRepository.save(member);
});
}


3)Board 테이블에 100 개의 데이터를 삽입
@Autowired
private BoardRepository boardRepository;

@Test
public void insertBoards() {
IntStream.rangeClosed(1, 100).forEach(i -> {
Member member = Member.builder().email("ggangpae1@aaa.com").build();
Board board = Board.builder()
.title("제목..." + i)
.content("내용..." + i)
.member(member)
.build();
boardRepository.save(board);

});
}

4)Reply 테이블에 300 개의 데이터를 삽입
@Autowired
private ReplyRepository replyRepository;

@Test
public void insertReplys() {
IntStream.rangeClosed(1, 300).forEach(i -> {
//1부터 100 사이의 정수를 랜덤하게 생성해서 Board 객체 생성
long bno = (long)(Math.random() * 100) + 1;
Board board = Board.builder().bno(bno).build();

Reply reply = Reply.builder()
.text("댓글..." + i)
.board(board)
.build();
replyRepository.save(reply);

});
}

7.Eager/Lazy Loading
1)하나의 Board 데이터를 가져오는 메서드를 테스트 - RepositoryTest 클래스에서 수행
//하나의 Board 데이터를 조회하는 메서드
@Test
public void readBoard() {
Optional<Board> result = boardRepository.findById(100L);
//데이터를 출력
System.out.println(result.get());
System.out.println(result.get().getMember());

}

=>수행된 쿼리를 확인해보면 Left Outer Join을 이용해서 데이터를 가져오는 것을 알 수 있습니다.


2)하나의 Reply 데이터를 가져오는 메서드를 테스트
//하나의 Reply 데이터를 조회하는 메서드
@Test
public void readReply() {
Optional<Reply> result = replyRepository.findById(410L);
//데이터를 출력
System.out.println(result.get());
System.out.println(result.get().getBoard());

}

=>수행된 쿼리를 확인해보면 Left Outer Join을 2번 이용

3)Eager Loading
=>즉시 로딩이라고 번역을 하는데 데이터를 조회할 때 관계가 있는 데이터를 바로 찾아오는 Loading 입니다.
=>관계를 만들 때 fetch 옵션을 생략하고 거나 직접 지정해서 사용
=>연관된 데이터를 자주 사용하는 경우가 아니라면 권장하지 않습니다.

4)Lazy Loading
=>지연 로딩이라고 번역을 하는데 연관된 데이터를 필요할 때 찾아오는 방식
=>관계를 설정할 때 fetch 옵션에 FetchType.LAZY를 적용하면 됩니다.


5)Board Entity에 Lazy Loading 적용
=>Board Entity 수정
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Board extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long bno;
private String title;
private String content;

//Member Entity를 N:1 관계로 참조
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}

=>이전의 하나의 Board를 가져오는 메서드를 다시 테스트 - 에러 발생
member 데이터를 찾아오지 않기 때문입니다.

=>데이터를 가져올 때 트랜잭션을 적용하면 이 문제가 해결됩니다.
테스트 하는 메서드 위에 @Transactional을 추가하고 다시 테스트

//하나의 Board 데이터를 조회하는 메서드
@Transactional
@Test
public void readBoard() {
Optional<Board> result = boardRepository.findById(100L);
//데이터를 출력
System.out.println(result.get());
System.out.println(result.get().getMember());

}

이 경우에는 join을 이용하지 않고 Board 의 데이터를 가져온 후 거기서 외래키의 값을 가지고 Member 테이블의 데이터를 다시 조회합니다.
서브쿼리의 형태로 동작합니다.

6)@ToString
=>ToString은 자신의 모든 속성의 toString을 호출해서 하나의 문자열로 만들어서 리턴하는 toString 메서드를 자동 생성해주는 어노테이션입니다.
=>Eager Loading을 사용하는 경우는 모든 데이터가 존재하기 때문에 별 문제가 안되지만 Lazy Loading의 경우는 처음에 연관된 데이터가 존재하지 않기 때문에 문제가 발생 가능성이 있음
=>@ToString(exclude="toString 만들 때 제외할 속성")을 이용해서 특정 속성을 제외할 수 있습니다.

=>Board에 적용
@ToString(exclude="member")


8.JPQL 과 left outer join
=>게시글을 가져올 때 댓글의 수를 같이 가져오고자 하는 경우에는 하나의 Entity로는 처리가 불가능
댓글의 수는 Entity에 존재하지 않기 때문
이런 경우에는 JPQL을 이용해서 해결 가능

1)BoardRepository 인터페이스에 Board 데이터를 가져올 때 Member 정보도 같이 가져오는 JPQL 처리 메서드를 생성
//Board 테이블에서 데이터를 가져올 대 Member 정보도 같이 가져오는 메서드
@Query("select b, w from Board b left join b.member w where b.bno = :bno")
Object getBoardWithMember(@Param("bno") Long bno);

2)RepositoryTest 클래스에서 앞에서 만든 메서드 테스트
@Test
public void testReadWithWriter() {
//데이터 조회
Object result = boardRepository.getBoardWithMember(100L);
//JPQL의 결과가 Object 인 경우는 Object[] 로 강제 형 변환해서 사용
//
System.out.println(Arrays.toString((Object[])result));
}


3)Board Repository 인터페이스에 글 번호에 해당하는 게시글 과 그 에 해당하는 모든 댓글을 가져오는 메서드를 작성
=>Board 테이블 과 Reply 테이블은 연관 관계가 있으면 Reply 테이블에 board 라는 속성으로 관계가 설정되어 있습니다.
Board에서 바라볼 때는 1:N 의 관계입니다.

@Query("select b, r from Board b left join Reply r on r.board = b where b.bno=:bno")
List<Object []> getBoardWithReply(@Param("bno") Long bno);


4)RepositoryTest 클래스에서 만든 메서드를 테스트
@Test
public void testGetBoardWithReply() {
List<Object []> result = boardRepository.getBoardWithReply(40L);
for(Object [] ar : result) {
System.out.println(Arrays.toString(ar));
}
}

9.목록 보기에 필요한 Repository 메서드 구현
1)필요한 데이터
Board: 게시물 번호, 제목, 작성 시간
Member: 회원의 이름, 이메일
Reply: 댓글 의 수 
페이징도 구현


2)BoardRepository 인터페이스에 데이터 목록을 가져오는 메서드를 선언
//목록 보기를 위한 메서드
@Query(value="select b, w,count(r) "
+ "from Board b left join b.member w left join Reply r On r.board = b "
+ "group by b",
   countQuery="select count(b) from Board b")
Page<Object []> getBoardWithReplyCount(Pageable Pageable);


3)RepositoryTest 클래스에서 테스트
@Test
public void testWithReplyCount() {
Pageable pageable = PageRequest.of(0,  10, Sort.by("bno").descending());
Page<Object[]> result = boardRepository.getBoardWithReplyCount(pageable);

result.get().forEach(row -> {
Object [] ar = (Object[])row;
System.out.println(Arrays.toString(ar));
});
}

=>결과에서 0번 요소가 Board 이고 1번 요소가 Member 2번 요소가 댓글의 개수


4)BoardRepository 인터페이스에 게시글 번호를 가지고 동일한 데이터를 가져오는 메서드를 선언
//게시글 번호를 가지고 데이터를 찾아오는 메서드
@Query(value="select b, w,count(r) "
+ "from Board b left join b.member w left join Reply r On r.board = b "
+ "where b.bno = :bno")
Object getBoardByBno(@Param("bno") Long bno);


5)RepositoryTest 클래스에서 메서드 테스트
@Test
public void testWithByBno() {
Object result = boardRepository.getBoardByBno(40L);

Object [] ar = (Object[])result;
System.out.println(Arrays.toString(ar));
}

10.CRUD 작업을 위한 준비
1)Board Entity에 해당하는 BoardDTO 클래스를 생성
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
private Long bno;
private String title;
private String content;
private LocalDateTime regDate;
private LocalDateTime modDate;

//작성자 정보
private String memberEmail;
private String memberName;

//댓글의 개수
private int replyCount;
}


2)Board Entity의 요청을 처리할 메서드의 원형을 소유할 BoardService 인터페이스를 생성하고 DTO 와 Entity 사이의 변환을 수행해주는 메서드를 생성 - service.BoardService

public interface BoardService {
//DTO를 Entity로 변환해주는 메서드
default Board dtoToEntity(BoardDTO dto) {
Member member = Member.builder().email(dto.getMemberEmail()).build();

Board board = Board.builder().bno(dto.getBno())
.title(dto.getTitle()).content(dto.getContent()).member(member).build();

return board;
}

//Entity를 DTO로 변환해주는 메서드
default BoardDTO entityToDTO(Board board, Member member, Long replyCount) {
BoardDTO boardDTO = BoardDTO.builder()
.bno(board.getBno()).title(board.getTitle()).content(board.getContent())
.regDate(board.getRegDate()).modDate(board.getModDate())
.memberEmail(member.getEmail()).memberName(member.getName())
.replyCount(replyCount.intValue())
.build();
return boardDTO;
}
}

3)사용자의 요청을 처리할 메서드를 구현할 BoardServiceImpl 클래스를 생성 - service.BoardServiceImpl
@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceimpl implements BoardService{
private final BoardRepository boardRepository;

}

11.게시물 등록
1)BoardService 인터페이스에 메서드를 선언
//게시물 등록을 위한 메서드 
Long register(BoardDTO dto);

2)BoardServiceImpl 클래스에 메서드 구현
@Override
public Long register(BoardDTO dto) {
log.info(dto);

Board board = dtoToEntity(dto);
boardRepository.save(board);
return board.getBno();
}

3)Service 계층 테스트를 위한 클래스를 생성하고 작성한 메서드 테스트 - src/test/java 의 ServiceTest
@SpringBootTest
public class ServiceTest {
@Autowired
private BoardService boardService;

@Test
public void testRegister() {
BoardDTO dto = BoardDTO.builder().title("Test").content("Test...")
.memberEmail("ggangpae55@aaa.com")
.build();

Long bno = boardService.register(dto);
System.out.println(bno);
}
}


12.게시물 목록 보기
1)목록 보기 요청을 위한 DTO 클래스를 생성 - dto.PageRequestDTO
@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
//현재 페이지 번호
private int page;
//페이지 당 출력할 데이터 개수
private int size;
//검색 항목
private String type;
//검색할 데이터
private String keyword;

//생성자
public PageRequestDTO() {
this.page = 1;
this.size = 10;
}

//페이지 검색 가능 객체 생성 메서드
public Pageable getPageable(Sort sort) {
return PageRequest.of(page-1,  size, sort);
}
}


2)목록 보기 응답을 위한 DTO 클래스를 생성 - dto.PageResultDTO
@Data
public class PageResultDTO <DTO, EN> {
//DTO 리스트
private List<DTO> dtoList;

//전체 페이지 개수
private int totalPage;

//현재 페이지 번호
private int page;
//출력할 페이지 번호 개수
private int size;

//시작하는 페이지 번호 와 끝나는 페이지 번호
private int start, end;

//이전 페이지 와 다음 페이지 존재 여부
private boolean prev, next;

//페이지 번호 목록
private List<Integer> pageList;

//페이지 번호 목록을 만들어주는 메서드
private void makePageList(Pageable pageable) {
this.page = pageable.getPageNumber() + 1;
this.size = pageable.getPageSize();

int tempEnd = (int)(Math.ceil(page/10.0)) * 10;
start = tempEnd - 9;
prev = start > 1;

end = totalPage > tempEnd ? tempEnd: totalPage;
next = totalPage > tempEnd;
pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
}

public PageResultDTO(Page <EN> result, Function<EN, DTO> fn) {
dtoList = result.stream().map(fn).collect(Collectors.toList());
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
}

3)BoardService 인터페이스에 목록 보기 메서드 선언
//목록 보기 메서드
PageResultDTO <BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO);

4)BoardServiceImpl 클래스에 목록 보기 메서드 구현
@Override
public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {
log.info(pageRequestDTO);

//Entity를 DTO로 변환해주는 함수 생성
//Repository의 메서드의 결과가 Object [] 인데 이 배열의 요소를 가지고
//BoardDTO를 생성해서 출력해야 함

Function<Object[], BoardDTO> fn = (en -> 
entityToDTO((Board)en[0], (Member)en[1], (Long)en[2]));

//데이터를 조회 - bno의 내림차순 적용
//상황에 따라서는 regDate 나 modDate로 정렬하는 경우도 있음
Page<Object[]> result = boardRepository.getBoardWithReplyCount(
pageRequestDTO.getPageable(Sort.by("bno").descending()));

return new PageResultDTO<>(result, fn);
}

5)ServiceTest 클래스에 앞에서 만든 메서드를 테스트하는 코드를 추가하고 확인
//@Test
public void testList() {
PageRequestDTO pageRequestDTO = new PageRequestDTO();

PageResultDTO<BoardDTO, Object[]> result = boardService.getList(pageRequestDTO);

for(BoardDTO boardDTO : result.getDtoList()){
System.out.println(boardDTO);
}
}


13.게시물 상세 보기
1)BoardService 인터페이스에서 상세보기(하나의 데이터를 자세히 확인하는 작업)를 위한 메서드를 선언
//상세보기 메서드
BoardDTO getBoard(Long bno);

2)BoardServiceImpl 클래스에 상세보기를 위한 메서드를 구현
@Override
public BoardDTO getBoard(Long bno) {
//bno을 이용해서 하나의 데이터 가져오기
//Board, Member, Long - 댓글 개수
Object result = boardRepository.getBoardByBno(bno);
Object [] ar = (Object []) result;

return entityToDTO((Board)ar[0], (Member)ar[1], (Long)ar[2]);

}

3)ServiceTest 클래스에 상세보기를 확인하는 메서드를 생성하고 확인
@Test
public void testGetBoard() {
Long bno = 40L;
BoardDTO boardDTO = boardService.getBoard(bno);
System.out.println(boardDTO);
}

14.게시물 삭제
=>삭제를 할 때는 실제 삭제할 것인지 아니면 삭제되었다는 표시를 할 것인지를 고민
=>Board 테이블에서 게시글을 지울 때 Reply 테이블에서 게시글에 해당하는 데이터도 삭제

1)ReplayRepository 인터페이스에 게시글 번호로 삭제하는 메서드를 생성
//게시글 번호를 이용해서 삭제하는 메서드
@Modifying
@Query("delete from Reply r where r.board.bno = :bno")
public void deleteByBno(@Param("bno") Long bno);

2)BoardService 에 게시글을 삭제하는 메서드를 선언
//게시글 삭제 메서드
void removeWithReplies(Long bno);

3)BoardServiceImpl 클래스에 게시글을 삭제하는 메서드를 구현
private final ReplyRepository replyRepository;

//이 메서드 안의 작업은 하나의 트랜잭션으로 처리해달라고 요청
@Transactional
@Override
public void removeWithReplies(Long bno) {
//댓글 삭제
replyRepository.deleteByBno(bno);
//게시글 삭제
boardRepository.deleteById(bno);

}

4)ServiceTest 클래스에 테스트 코드를 작성하고 테스트
@Test
public void testDeleteBoard() {
Long bno = 40L;
boardService.removeWithReplies(bno);
}


15.데이터 수정
1)Board Entity에 title 과 content를 수정할 수 있는 메서드를 추가
//title을 수정하는 메서드
public void changeTitle(String title) {
this.title = title;
}

//content를 수정하는 메서드
public void changeContent(String content) {
this.content = content;
}

2)BoardService 인터페이스에 게시글 수정을 위한 메서드를 선언
//게시글 수정 메서드
void modifyBoard(BoardDTO boardDTO);

3)BoardServiceImpl 클래스에 게시글 수정을 위한 메서드를 구현
@Transactional
@Override
public void modifyBoard(BoardDTO boardDTO) {
//데이터의 존재 여부를 확인
Optional<Board> board = boardRepository.findById(boardDTO.getBno());
if(board.isPresent()) {
board.get().changeTitle(boardDTO.getTitle());
board.get().changeContent(boardDTO.getContent());

boardRepository.save(board.get());
}

}

4)ServiceTest 클래스에서 수정 메서드 테스트
@Test
public void testModifyBoard() {
BoardDTO boardDTO = BoardDTO.builder().bno(1L)
.title("제목을 수정").content("내용을 수정").build();
boardService.modifyBoard(boardDTO);
}


16.Controller 와 View 계층
1)공통된 디자인 적용을 위한 설정 - bootstrap 의 simple sidebar 디자인 적용
=>공통된 디자인을 적용하기 위한 css 파일이나 js 파일은 static 디렉토리에 위치시켜야 합니다. 

=>이전에 사용했던 assets, css, js 디렉토리를 src/main/resources 디렉토리 안의 static 디렉토리에 복사

=>이전에 공통된 메뉴를 위해 만들었던 basic.html 파일을 src/main/resources 디렉토리 안의 template 디렉토리에 layout 디렉토리를 만들고 복사


2)Controller 역할을 수행할 BoardController 클래스를 생성
@Controller
@Log4j2
@RequiredArgsConstructor
public class BoardController {

private final BoardService boardService;
}

3)목록보기 처리
=>목록보기 요청을 처리할 메서드를 BoardController 클래스에 생성
//목록 보기 요청을 처리할 메서드
@GetMapping({"/", "/board/list"})
public String list(PageRequestDTO pageRequestDTO, Model model) {
log.info("목록 보기 요청..." + pageRequestDTO);
//서비스 메서드를 호출해서 결과를 저장
model.addAttribute("result", boardService.getList(pageRequestDTO));

return "board/list";
}

=>templates 디렉토리에 board 디렉토리를 생성하고 list.html 파일을 생성하고 작성
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

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

<h1 class="mt-4">
Board List Page 
<span> 
<a th:href="@{/board/register}">
<button type="button" class="btn btn-outline-primary">REGISTER
</button>
</a>
</span>
</h1>

<form action="/board/list" method="get" id="searchForm">
<div class="input-group">
<input type="hidden" name="page" value="1">
<div class="input-group-prepend">
<select class="custom-select" name="type">
<option th:selected="${pageRequestDTO.type == null}">-------</option>
<option value="t" th:selected="${pageRequestDTO.type =='t'}">제목</option>
<option value="c" th:selected="${pageRequestDTO.type =='c'}">내용</option>
<option value="w" th:selected="${pageRequestDTO.type =='w'}">작성자</option>
<option value="tc" th:selected="${pageRequestDTO.type =='tc'}">제목
+ 내용</option>
<option value="tcw" th:selected="${pageRequestDTO.type =='tcw'}">제목
+ 내용 + 작성자</option>
</select>
</div>

<input class="form-control" name="keyword"
th:value="${pageRequestDTO.keyword}">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary btn-search" type="button">Search</button>
<button class="btn btn-outline-secondary btn-clear" type="button">Clear</button>
</div>
</div>
</form>

<table class="table table-striped">
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">작성자</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}">
<th scope="row">
[[${dto.bno}]]</th>
<td><a
th:href="@{/board/read(bno = ${dto.bno},
                    page= ${result.page},
                    type=${pageRequestDTO.type} ,
                    keyword = ${pageRequestDTO.keyword})}">[[${dto.title}]] ---------------- [<b
th:text="${dto.replyCount}"></a></b>]
</td>
<td>[[${dto.memberName}]]
</td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>

<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item " th:if="${result.prev}"><a
class="page-link"
th:href="@{/board/list(page= ${result.start -1},
                    type=${pageRequestDTO.type} ,
                    keyword = ${pageRequestDTO.keyword} ) }"
tabindex="-1">Previous</a></li>
<li th:class=" 'page-item ' + ${result.page == page?'active':''} "
th:each="page: ${result.pageList}"><a class="page-link"
th:href="@{/board/list(page = ${page} ,
                   type=${pageRequestDTO.type} ,
                   keyword = ${pageRequestDTO.keyword}  )}">
[[${page}]] </a></li>
<li class="page-item" th:if="${result.next}"><a
class="page-link"
th:href="@{/board/list(page= ${result.end + 1} ,
                    type=${pageRequestDTO.type} ,
                    keyword = ${pageRequestDTO.keyword} )}">Next</a>
</li>
</ul>

<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">메시지 확인</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>[[${msg}]]</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

<script th:inline="javascript">
       var msg = [[${msg}]];
       console.log(msg);

       if(msg){
         $(".modal").modal();
       }
      
       var searchForm = $("#searchForm");
      
       $('.btn-search').click(function(e){
         searchForm.submit();
       });

       $('.btn-clear').click(function(e){
         searchForm.empty().submit();
       });

</script>
</th:block>
</th:block>

4)게시물 작성
=>BoardController 클래스에 게시물 등록을 위한 메서드를 생성
//게시물 작성을 처리할 메서드
@GetMapping("/board/register")
public void register() {
log.info("게시물 등록으로 이동");
}


@PostMapping("/board/register")
public String register(BoardDTO dto, RedirectAttributes redirectAttributes) {
log.info("게시물 처리 중.." + dto);
//게시물 등록
Long bno = boardService.register(dto);
//View에 데이터 전달
redirectAttributes.addFlashAttribute("msg", bno + " 삽입");

return "redirect:/board/list";
}

=>templates/board 디렉토리에 register.html 파일을 생성하고 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
  <th:block th:fragment="content">
    <h1 class="mt-4">게시물 작성</h1>

    <form th:action="@{/board/register}" th:method="post">
      <div class="form-group">
        <label>제목</label>
        <input type="text" class="form-control" name="title" placeholder="Enter Title">
      </div>
      <div class="form-group">
        <label>내용</label>
        <textarea class="form-control" rows="5" name="content"></textarea>
      </div>
      <div class="form-group">
        <label>작성자 이메일</label>
        <input type="email" class="form-control" name="memberEmail" placeholder="작성자 이메일">
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>

  </th:block>

</th:block>
      

5)게시물 상세 보기
1)BoardController에 상세보기 요청을 처리할 메서드를 생성
//상세보기 처리를 위한 메서드
@GetMapping("/board/read")
//@ModelAttribute("이름") 파라미터를 받아서 이름으로 다음 요청에게 넘겨주는 역할을 수행
public void read(@ModelAttribute("requestDTO") PageRequestDTO pageRequestDTO, 
Long bno, Model model) {
log.info("상세보기 처리 중.." + bno);

BoardDTO boardDTO = boardService.getBoard(bno);
model.addAttribute("dto", boardDTO);
}

2)templates/board 디렉토리에 read.html 파일을 생성하고 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
  <th:block th:fragment="content">
    <h1 class="mt-4">게시물 상세 보기</h1>

    <div class="form-group">
      <label >글번호</label>
      <input type="text" class="form-control" name="gno" th:value="${dto.bno}" readonly >
    </div>
    <div class="form-group">
      <label >제목</label>
      <input type="text" class="form-control" name="title" th:value="${dto.title}" readonly >
    </div>
    <div class="form-group">
      <label >내용</label>
      <textarea class="form-control" rows="5" name="content" readonly>[[${dto.content}]]</textarea>
    </div>
    <div class="form-group">
      <label >작성자</label>
      <input type="text" class="form-control" name="member" th:value="${dto.memberName}" readonly>
    </div>
    <div class="form-group">
      <label >작성일</label>
      <input type="text" class="form-control" name="regDate" th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
    </div>
    <div class="form-group">
      <label >최종 수정일</label>
      <input type="text" class="form-control" name="modDate" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
    </div>

        <a th:href="@{/board/modify(bno = ${dto.bno}, page=${requestDTO.page}, type=${requestDTO.type}, keyword =${requestDTO.keyword})}">
      <button type="button" class="btn btn-primary">수정</button>
    </a>

    <a th:href="@{/board/list(page=${requestDTO.page} , type=${requestDTO.type}, keyword =${requestDTO.keyword})}">
      <button type="button" class="btn btn-info">목록</button>
    </a>


  </th:block>

</th:block>
    

6)게시물 수정 및 삭제
1)BoardController 에서 게시물 상세보기 요청을 처리하는 부분의 URL을 수정
//상세보기 와 수정보기 처리를 위한 메서드
@GetMapping({"/board/read", "/board/modify"})
//@ModelAttribute("이름") 파라미터를 받아서 이름으로 다음 요청에게 넘겨주는 역할을 수행
public void read(@ModelAttribute("requestDTO") PageRequestDTO pageRequestDTO, 
Long bno, Model model) {
log.info("상세보기 처리 중.." + bno);

BoardDTO boardDTO = boardService.getBoard(bno);
model.addAttribute("dto", boardDTO);
}
=>상세보기 와 수정을 위한 화면으로 이동하는 것은 하나의 데이터를 찾아오는 것은 동일하고 출력할 때 읽기 전용으로 만들것이냐 아니면 편집이 가능하도록 할 것이냐의 차이입니다.

2)BoardController 클래스에 수정 과 삭제를 처리할 메서드를 추가
//수정을 처리할 메서드
@PostMapping("/board/modify")
//수정은 이전에 보고 있던 목록 보기로 돌아갈수 있어야 하기 때문에 목록 보기에 필요한 데이터가 필요합니다.
public String modify(BoardDTO dto, @ModelAttribute("requestDTO") PageRequestDTO requestDTO,
RedirectAttributes redirectAttributes) {
log.info("수정 처리 중.." + dto);

boardService.modifyBoard(dto);

redirectAttributes.addAttribute("page", requestDTO.getPage());
redirectAttributes.addAttribute("type", requestDTO.getType());
redirectAttributes.addAttribute("keyword", requestDTO.getKeyword());
redirectAttributes.addAttribute("bno", dto.getBno());

return "redirect:/board/read";
}

//삭제를 처리할 메서드
@PostMapping("/board/remove")
public String remove(long bno, RedirectAttributes redirectAttributes) {
log.info("삭제 처리..." + bno);

boardService.removeWithReplies(bno);
redirectAttributes.addFlashAttribute("msg", bno + " 삭제");

return "redirect:/board/list";
}

3)templates/board 디렉토리에 modify.html 파일을 생성하고 작성
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">
<th:block th:fragment="content">
<h1 class="mt-4">Board Modify Page</h1>
<form action="/board/modify" method="post">

<!--페이지 번호  -->
<input type="hidden" name="page" th:value="${requestDTO.page}">
<input type="hidden" name="type" th:value="${requestDTO.type}">
<input type="hidden" name="keyword" th:value="${requestDTO.keyword}">

<div class="form-group">
<label>Bno</label> <input type="text" class="form-control"
name="bno" th:value="${dto.bno}" readonly>
</div>

<div class="form-group">
<label>Title</label> <input type="text" class="form-control"
name="title" th:value="${dto.title}">
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" rows="5" name="content">[[${dto.content}]]</textarea>
</div>
<div class="form-group">
<label>Writer</label> <input type="text" class="form-control"
name="member" th:value="${dto.memberEmail}" readonly>
</div>
<div class="form-group">
<label>RegDate</label> <input type="text" class="form-control"
th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}"
readonly>
</div>
<div class="form-group">
<label>ModDate</label> <input type="text" class="form-control"
th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}"
readonly>
</div>
</form>

<button type="button" class="btn btn-primary modifyBtn">수정</button>
<button type="button" class="btn btn-info listBtn">목록</button>
<button type="button" class="btn btn-danger removeBtn">삭제</button>

<script th:inline="javascript">
      var actionForm = $("form"); //form 태그 객체

      $(".removeBtn").click(function(){
          if(!confirm("삭제하시겠습니까?")){
          return ;
        }        
           actionForm
                .attr("action", "/board/remove")
                .attr("method","post");

        actionForm.submit();
      });

      $(".modifyBtn").click(function() {
        if(!confirm("수정하시겠습니까?")){
          return ;
        }
        actionForm
                .attr("action", "/board/modify")
                .attr("method","post")
                .submit();
      });
      
      $(".listBtn").click(function() {
        //var pageInfo = $("input[name='page']");
        var page = $("input[name='page']");
        var type = $("input[name='type']");
        var keyword = $("input[name='keyword']");

        actionForm.empty(); //form 태그의 모든 내용을 지우고

        actionForm.append(page);
        actionForm.append(type);
        actionForm.append(keyword);
        
        actionForm
                .attr("action", "/board/list")
                .attr("method","get");

        actionForm.submit();
      })
    </script>

</th:block>
</th:block>



17.동적인 쿼리 작업
=>검색 항목이 고정이 아니고 변하는 쿼리 - Spring JPA에서는 querydsl을 이용해서 처리 가능

1)querydsl 사용 설정 

=>querydsl을 사용하기 위해서 build.gradle을 수정: group, version, sourceCompatibility,configurations, tasks.named('test') 는 그대로 implementation에서는 jpa 와 apt 만 추가

buildscript{
ext{
queryDslVersion = "5.0.0"
}
}

plugins {
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'

id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'com.adamsoft'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

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'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time'

implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl{
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets{
main.java.srcDir querydslDir
}
configurations{
compileOnly{
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}


tasks.named('test') {
useJUnitPlatform()
}

=>build tasks 윈도우를 열어서 build 에서 build 와 jar을 더블 클릭 합니다.

=>build.gradle을 수정 후 프로젝트를 선택하고 마우스 오른쪽을 누른 후 [gradle] - [refresh gradle project]를 수행

=>제대로 빌드되었다면 프로젝트에 build/generated/querydsl 디렉토리가 생성됩니다.


2)persistence 패키지에 검색을 위한 SearchBoardReposity 인터페이스를 생성 - BoardRepository의 기능을 확장하기 위해서 만든 인터페이스

public interface SearchBoardRepository {

}

3)persistence 패키지에 검색을 위한 SearchBoardReposity 인터페이스를 implements 하고 Querydsl을 사용할 수 있는 클래스를 생성 - SearchBoardRepositoryImpl

public class SearchBoardRepositoryImpl 
extends QuerydslRepositorySupport implements SearchBoardRepository {

public SearchBoardRepositoryImpl() {
super(Board.class);
}

}

4)SearchBoardRepository 인터페이스에 메서드 선언
Board search();

5)SearchBoardRepositoryImpl 클래스에 메서드 구현
@Override
public Board search() {
log.info("search...");

QBoard board = QBoard.board;

JPQLQuery<Board> jpqlQuery = from(board);
//bno 가 50인 데이터 조회를 위한 메서드 호출
jpqlQuery.select(board).where(board.bno.eq(50L));

//실행
List<Board> result = jpqlQuery.fetch();

log.info("결과:" + result);

return null;
}


6)BoardRepository 인터페이스에 SearchBoardRepository를 extends
public interface BoardRepository extends JpaRepository<Board, Long>, SearchBoardRepository{

7)RepositoryTest 클래스에 테스트 메서드를 생성하고 확인
@Test
public void testSearch() {
boardRepository.search();
}

8)SearchBoardRepositoryImpl 클래스의 search 메서드를 수정하고 테스트
@Override
public Board search() {
//쿼리를 수행할 수 있는 Querydsl 객체를 찾아옵니다.
QBoard board = QBoard.board;
QMember member = QMember.member;
QReply reply = QReply.reply;

//쿼리 객체를 생성
JPQLQuery<Board> jpqlQuery = from(board);

//member 와 join
jpqlQuery.leftJoin(member).on(board.member.eq(member));
//reply 와 join
jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

//필요한 데이터를 조회하는 구문을 추가
//조인한 데이터를 board 별로 묶어서 board 와 회원의 email 그리고 댓글의 개수 조회
jpqlQuery.select(board, member.email, reply.count()).groupBy(board);

//결과 가져오기
List<Board> result = jpqlQuery.fetch();

System.out.println(result);
return null;
}

9)SearchBoardRepositoryImpl 클래스의 search 메서드를 수정하고 테스트
//결과를 Tuple로 받기
QBoard board = QBoard.board;
QMember member = QMember.member;
QReply reply = QReply.reply;

//Tuple은 관계형 데이터베이스에서는 하나의 행을 지칭하는 용어
//프로그래밍에서는 일반적으로 여러 종류의 데이터가 묶여서 하나의 데이터를 나타내는 자료형
//Map 과 다른 점은 Map은 key로 세부 데이터를 접근하지만 Tuple은 인덱스로도 접근이 가능하고
//대부분의 경우 Tuple은 수정이 불가능
JPQLQuery<Board> jpqlQuery = from(board);
//member 와 join
jpqlQuery.leftJoin(member).on(board.member.eq(member));
//reply 와 join
jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member.email, reply.count());
tuple.groupBy(board);

//결과 가져오기
List<Tuple> result = tuple.fetch();

System.out.println(result);
return null;


10)SearchBoardRepository 인터페이스에 검색 기능을 위한 메서드를 선언 
//검색을 위한 메서드
//3개의 항목을 묶어서 하나의 클래스로 표현해도 됩니다.
Page <Object []> searchPage(String type, String keyword, Pageable pageable);

11)SearchBoardRepositoryImpl 클래스에 검색 기능을 위한 메서드를 구현
@Override
public Page<Object[]> searchPage(String type, String keyword, Pageable pageable) {
//결과를 Tuple로 받기
QBoard board = QBoard.board;
QMember member = QMember.member;
QReply reply = QReply.reply;

//Tuple은 관계형 데이터베이스에서는 하나의 행을 지칭하는 용어
//프로그래밍에서는 일반적으로 여러 종류의 데이터가 묶여서 하나의 데이터를 나타내는 자료형
//Map 과 다른 점은 Map은 key로 세부 데이터를 접근하지만 Tuple은 인덱스로도 접근이 가능하고
//대부분의 경우 Tuple은 수정이 불가능
JPQLQuery<Board> jpqlQuery = from(board);
//member 와 join
jpqlQuery.leftJoin(member).on(board.member.eq(member));
//reply 와 join
jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member, reply.count());

//동적인 쿼리 수행을 위한 객체 생성
BooleanBuilder booleanBuilder = new BooleanBuilder();
//bno 가 0보다 큰 데이터를 추출하는 조건
BooleanExpression expression = board.bno.gt(0L);

//type이 검색 항목
if(type != null) {
String [] typeArr = type.split("");
BooleanBuilder conditionBuilder = new BooleanBuilder();
for(String t:typeArr) {
switch(t) {
case "t":
conditionBuilder.or(board.title.contains(keyword));
break;
case "c":
conditionBuilder.or(board.content.contains(keyword));
break;
case "w":
conditionBuilder.or(member.email.contains(keyword));
break;
}

}
booleanBuilder.and(conditionBuilder);
}

//조건 적용
tuple.where(booleanBuilder);

//데이터 정렬 - 하나의 조건으로만 정렬
//tuple.orderBy(board.bno.desc());

//정렬 조건 가져오기
Sort sort = pageable.getSort();

//설정된 모든 정렬 조건을 순회해서 tuple에 적용
sort.stream().forEach(order -> {
Order direction = order.isAscending()?Order.ASC:Order.DESC;
String prop = order.getProperty();

PathBuilder orderByExpression = new PathBuilder(Board.class, "board");
tuple.orderBy(new OrderSpecifier(direction, orderByExpression.get(prop)));
});

//그룹화
tuple.groupBy(board);

//페이징 처리
tuple.offset(pageable.getOffset());
tuple.limit(pageable.getPageSize());

//결과를 가져오기
List<Tuple> result = tuple.fetch();

//결과를 리턴
return new PageImpl<Object[]>(
result.stream().map(t->t.toArray()).collect(Collectors.toList()),
pageable,
tuple.fetchCount());

}


12)RepositoryTest 클래스에서 만든 메서드 테스트
@Test
public void testSearchPage() {
Pageable pageable = PageRequest.of(0,  10, 
Sort.by("bno").descending().and(Sort.by("title").ascending()));
Page<Object[]> result = boardRepository.searchPage("t", "1", pageable);
System.out.println(result);
}

13)BoardServiceImpl 클래스의 getList 메서드 수정
@Override
public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {
log.info(pageRequestDTO);

//Entity를 DTO로 변환해주는 함수 생성
//Repository의 메서드의 결과가 Object [] 인데 이 배열의 요소를 가지고
//BoardDTO를 생성해서 출력해야 함

Function<Object[], BoardDTO> fn = (en -> 
entityToDTO((Board)en[0], (Member)en[1], (Long)en[2]));

//데이터를 조회 - bno의 내림차순 적용
//상황에 따라서는 regDate 나 modDate로 정렬하는 경우도 있음
/*
Page<Object[]> result = boardRepository.getBoardWithReplyCount(
pageRequestDTO.getPageable(Sort.by("bno").descending()));
*/

Page<Object[]> result = boardRepository.searchPage(
pageRequestDTO.getType(), 
pageRequestDTO.getKeyword(),
pageRequestDTO.getPageable(Sort.by("bno").descending()));


return new PageResultDTO<>(result, fn);
}



18.댓글 작업
1)개요
=>댓글을 처리하는 Controller는 REST Controller를 이용해서 생성
=>웹 클라이언트에서 ajax를 이용해서 댓글을 처리
=>URL 과 전송 방식
댓글 가져오기 - /replies/board/{bno - 게시글 번호} - GET 방식, JSON 배열 리턴
댓글 작성 - /replies - POST 방식, 추가된 댓글번호
댓글 삭제 - /replies/{rno} - DELETE 방식, 삭제 결과 문자열
댓글 수정 - /replies/{rno} - PUT 방식, 수정 결과 문자열

2)Reply Entity를 수정
@Entity

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
//toString 메서드를 만들 때 board는 제외 - FetchType.Lazy를 적용하면 에러가 발생
@ToString(exclude="board")
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long rno;
private String text;
private String replyer;

//다대일 관계이고 데이터는 처음부터 가져오지 않고 나중에 가져오는 것으로 설정
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
}

3)ReplyRepository 클래스에 게시글 번호를 이용해서 댓글 목록을 가져오는 메서드를 추가
//게시글 번호를 이용해서 댓글 목록을 가져오는 메서드
public List<Reply> getRepliesByBoardOrderByRno (Board board);

4)Test 클래스에서 테스트
=>데이터베이스에서 댓글이 있는 게시글 번호를 먼저 찾기
-- reply 테이블에서 board_bno로 그룹화해서  board_bno 와 데이터 개수를 조회
-- 데이터 개수의 내림차순으로 정렬
select board_bno, count(*) from reply group by board_bno order by 2 desc;


=>테스트 클래스에서 작성하고 실행
@Test
public void testListByBoard() {
List<Reply> replyList = 
replyRepository.getRepliesByBoardOrderByRno(Board.builder().bno(7L).build());
System.out.println(replyList);
}

5.Reply Entity를 화면에 출력하기 위한 ReplyDTO 클래스를 생성
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
private Long rno;
private String text;
private String replyer;

private Long bno;

private LocalDateTime regDate;
private LocalDateTime modDate;
}










    'Spring' 카테고리의 다른 글
    • [SpringBoot] Git 연동하기
    • [SpringBoot] / 2022/05/31 ManyToMany
    • [SpringBoot] 2022/05/24 ToDo (REST API)
    • [SpringBoot] 2022/05/24 Memo

    티스토리툴바