본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 댓글 프로젝트] 3-1. N:1 연관관계의 게시물과 댓글 CRUD

반응형

요구사항

1. N:1 연관관계를 이용해, 게시글, 댓글, 회원 엔티티 작성

2. CRUD를 이용해 게시글, 댓글, 회원의 추가, 수정, 삭제  메서드 생성

3. RESTful을 이용해, JSON으로, 댓글은 Ajax를 이용해 비동기 처리

 

필수 과제

1. @ManyToOne 다대일 연관관계를 설정

2. 연관관계가 없는 상황에서 left (outer) join 처리 방법

3. 즉시 로딩과 지연 로딩 차이와 효율적인 처리 방법

 


연관관계와 관계형 데이터베이스 설계

:PK와 FK의 설정

 

고유한 키값을 가지는 PK를 여러개 FK에서 참조하는 관계

: @ManyToOne

-> 특정한 PK가 다른 곳에서 몇번 FK로 사용되는지 파악

 

한 명의 회원은 여러 개의 게시글 작성 가능
하나의 게시글은 한 명의 작성자만 표시

즉,
하나의 게시글은 여러 개의 댓글을 가질 수 있다.
하나의 댓글은 하나의 게시글에 속한다.

-> FK는 객체의 참조의 형태로 관계를 설정한다. 

 

회원 테이블 설계 -> 게시글 테이블 설계 -> 댓글 테이블 설계
-> 게시글과 회원 : N:1 관계 [게시글이 회원을 참조]
-> 댓글과 게시글 : N:1 관계 [댓글이 게시글을 참조]

N쪽이 연관관계의 주인이기 때문에

 


1. 엔티티 생성

 

Member는 이메일 주소를 PK로 한다.

Board는 글번호를 PK로 하며, Member의 이메일을 FK로 참조

Reply는 댓글번호를 FK로 하며, 회원이 아닌 사람도 댓글 남길 수 있게 설정하지만 Board의 PK인 gno를 참조

 

-> N:1관계에서 N에 해당하는 필드에 @ManyToOne설정 [기본 값이 EAGER이므로, LAZY 설정] -> Test시 @Transactional 설정

-> ToSttring 어노테이션에 연관관계 멤버 변수를 제외시킨다.

 

package com.board.boot3.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
@EntityListeners(value = { AuditingEntityListener.class })
@Getter
abstract class BaseEntity {

    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name ="moddate")
    private LocalDateTime modDate;

}

-> 리스너를 이용하므로, 메인클래스에 처리 필요

 

package com.board.boot3;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

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

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

}

 

package com.board.boot3.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
//연관관계가 존재할 경우 ToString에서 해당 연관을 제외시켜준다.
@ToString(exclude = "writer")
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    private String title;

    private String content;

    //일대일 관계, 다대일 관계는 글로벌 패치 전략을 지연으로 설정
    @ManyToOne (fetch = FetchType.LAZY)
    private Member writer;

    public void changeTitle(String title){
        this.title = title;
    }

    public void changeContent(String content){
        this.content = content;
    }
}

 

package com.board.boot3.entity;

import lombok.*;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Table(name = "tbl_member")
public class Member extends BaseEntity {

    @Id
    private String email;

    private String password;

    private String name;

}

 

package com.board.boot3.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "board")
//연관관계가 존재할 경우 ToString에서 해당 연관을 제외시켜준다.
public class Reply extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;

    private String text;

    private String replyer;

    @ManyToOne
    private Board board;

}

-> 연관관계

 

2. 더미 데이터 생성/조회 테스트

 

현재 화면과 필요한 데이터 리스트

  • 목록 화면
    • 게시글 번호 제목 댓글 개수 작성자의 이름/이메일
  • 조회 화면
    • 게시글의 번호, 제목, 내용, 댓글 개수, 작성자의 이름/이메일

-> 연관관계를 이용해 구해야 하는 데이터이다.

 

@MayToOne과 Eager/Lazy loading

: 엔티티 클래스들은 DB상에는 여러 개의 테이블이므로, 조인이 필요하다

-> 따라서 다대일 연관관계에서 FK 엔티티를 가져올 경우 PK의 엔티티도 불러온다.

 

reply 처리의 경우 member 테이블, board 테이블, reply 테이블까지 모두 조인이 진행된다.

-> 효율적이지 않은 조인

 

따라서, 해당 경우처럼 엔티티 조회시 연관테이블의 모두 조인이 진행되는 것을 Eager Loading이라고 하고,

즉시 로딩이 진행되면, 성능 저하가 발생한다.

 

JPA에서는 이러한 상황을 방지하기 위해 Fetch 모드를 지정할 수 있다.

(fetch : 연관관계의 데이터를 어떻게 가져올 것인지 결정하는 것)

 

:Lazy Loading 을 이용해 성능 저하를 막는다

(실제로 연관된 엔티티를 사용할 경우에 로딩하는 방식)

 

 

package com.board.boot3.repository;


import com.board.boot3.entity.Board;
import com.board.boot3.entity.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

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

@SpringBootTest
public class BoardRepositoryTests {

    @Autowired
    private BoardRepository boardRepository;

    @Test
    public void insertBoard() {

        IntStream.rangeClosed(1,100).forEach(i -> {

            Member member = Member.builder().email("user"+i +"@aaa.com").build();

            Board board = Board.builder()
                    .title("Title..."+i)
                    .content("Content...." + i)
                    .writer(member)
                    .build();

            boardRepository.save(board);

        });

    }


    //지연로딩을 할 경우 @Transactional 필요
    //writer를 가져오기위해 member테이블을 로딩해야하는데 연결이 이미 끝난상태이기 때문에
    @Transactional
    @Test
    public void testRead1() {

        Optional<Board> result = boardRepository.findById(100L); //데이터베이스에 존재하는 번호

        Board board = result.get();

        System.out.println(board);
        System.out.println(board.getWriter());

    }


}

-> 처음에 board테이블만 로딩하고, .getWriter() 호출 시 member 테이블 로딩한다.

[즉시로딩과의 차이점]

 

package com.board.boot3.repository;


import com.board.boot3.entity.Member;
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 MemberRepositoryTests {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    public void insertMembers() {

        IntStream.rangeClosed(1, 100).forEach(i -> {

            Member member = Member.builder()
                    .email("user" + i + "@aaa.com")
                    .password("1111")
                    .name("USER" + i)
                    .build();

            memberRepository.save(member);
        });
    }
}

 

package com.board.boot3.repository;
import com.board.boot3.entity.Board;
import com.board.boot3.entity.Reply;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
public class ReplyRepositoryTests {

    @Autowired
    private ReplyRepository replyRepository;

    @Test
    public void insertReply() {

        IntStream.rangeClosed(1, 300).forEach(i -> {
            //1부터 100까지의 임의의 번호
            long bno  = (long)(Math.random() * 100) + 1;

            Board board = Board.builder().bno(bno).build();

            Reply reply = Reply.builder()
                    .text("Reply......." +i)
                    .board(board)
                    .replyer("guest")
                    .build();

            replyRepository.save(reply);

        });

    }
    @Test
    public void readReply1() {

        Optional<Reply> result = replyRepository.findById(1L);

        Reply reply = result.get();

        System.out.println(reply);
        System.out.println(reply.getBoard());

    }

}

 

-> 지연 로딩 사용시, 연관관계가 복잡할 경우 쿼리가 여러번 실행 될 수 있다.

 

 


3. JPQL과 left(outer) join

: 목록에서 게시글 정보와 함께 댓글의 수를 가져오려면 -> 조인이 반드시 필요

 

(1) 조인

내부조인 : INNER JOIN / JOIN

-> 교집합만 출력

외부조인 : LEFT OUTER JOIN / LEFT JOIN

-> 왼쪽 테이블 기준으로 테이블 전체와 교집합

 

-> 연관관계의 경우 ON이 없고, 외부에 존재할 경우 ON을 이용해 조인 조건을 명시한다.

 

(2) 엔티티 클래스 내부의 연관관계

-> Board 엔티티 내부 Member 엔티티 클래스를 변수로 선언해 참조한다.

따라서 Board의 writer 변수를 이용해 조인 처리

 

(3) 조인을 통한 조회 메서드

 

   //연관관계가 있는 글과 작성자 정보 조회 메서드
    @Query("select b, w from Board b left join b.writer w where b.bno =:bno")
    //한 개의 로우(Object) 내에 Object[]로 나옴
    Object getBoardWithWriter(@Param("bno") Long bno);
    //Board를 사용하지만, Member를 함께 조회 -> 따라서 b.writer로 사용
    //-> 내부의 엔티티 사용시에는 ON을 붙이지 않는다.
    //연관관계가 없는, 글과 댓글 정보 조회 메서드
    @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);

 

 

글과 작성자 정보 조회 테스트

  //연관관계에 있는 조인 테스트 : 글과 작성자 정보
    @Test
    public void testReadWithWriter() {

        Object result = boardRepository.getBoardWithWriter(100L);

        Object[] arr = (Object[])result;

        System.out.println("-------------------------------");
        System.out.println(Arrays.toString(arr));

    }

글과 댓글 정보 조회 테스트

 

    //연관관계가 없는 조인 테스트 : 글과 댓
    @Test
    public void testGetBoardWithReply() {

        List<Object[]> result = boardRepository.getBoardWithReply(100L);

        for (Object[] arr : result) {
            System.out.println(Arrays.toString(arr));
        }
    }

4. 목록 화면을 위한 JPQL

 

목록 화면 구성 데이터

  • 게시물
    • 게시물 번호, 제목, 게시물 작성 시간
  • 회원
    • 회원 이름 / 이메일
  • 댓글
    • 해당 게시물의 댓글 수

-> 데이터가 가장 많은 Board 기준 조인 후, Board 기준 GROUP BY를 통해 하나의 게시물 당 하나의 라인으로 처리

[Memer는 Board 내 writer 필드와 연관관계, Reply는 연관관계가 없어서, ON을 이용한 조인 조건 명시 필요]

 

//목록 화면 출력을 위한 메서드 : 오라클에서 발생하는 GROUP BY 오류 -> group by 하나씩 적용하거나 서브쿼리
    @Query(value ="SELECT b.bno , min(b.title) , min(w.email), count(r) " +
            " FROM Board b " +
            " LEFT JOIN b.writer w " +
            " LEFT JOIN Reply r ON r.board = b " +
            " GROUP BY b",
            countQuery ="SELECT count(b) FROM Board b")
    Page<Object[]> getBoardWithReplyCount(Pageable pageable);

 

 목록 화면 출력 테스트 : Board 기준 조인 후, Board 기준 GROUP BY과 페이징과 정렬

 //목록 화면 출력 테스트 : Board 기준 조인 후, Board 기준 GROUP BY과 페이징과 정렬
    @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[] arr = (Object[])row;

            System.out.println(Arrays.toString(arr));
        });
    }

 

5. 엔티티와 다른 점이 존재하는 DTO

: 뷰에 전달하는 목적으로 사용하기 때문에

-> Member 참조하지 않으며, 대신 작성자 이름, 작성자 이메일 정보로 처리한다.

-> 또한, 목록 화면에서 이용하기 때문에 댓글 수를 위한 변수 추가

더보기

게시물 DTO와 페이징 처리를 위한 DTO 작성

package com.board.boot3.dto;


import lombok.*;

import java.time.LocalDateTime;
//DTO는 뷰를 처리하므로, Getter, Setter 생성
//뷰를 위한 데이터이기 때문에 엔티티와 달리 Member 참조는 없다.
//-> 작성자 이메일과 작성자 이름으로 처리 또한 목록 화면에서 댓글 수를 전달하기 위해 댓글 수 변수 추가
@Data
@ToString
@Builder
//인자가 모두 존재하는 생성자, 기본 생성자 자동생성
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {

    private Long bno;

    private String title;

    private String content;

    private String writerEmail; //작성자의 이메일(id)

    private String writerName; //작성자의 이름

    private LocalDateTime regDate;

    private LocalDateTime modDate;

    private int replyCount; //해당 게시글의 댓글 수


}

 

package com.board.boot3.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;


//화면에 전달되는 목록
@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
    private int page;
    private int size;

    //화면에 전달되는 목록을 위한 생성자 : 1페이지부터 시작하고, 10페이지 단위로 목록 처리를 의미
    public PageRequestDTO(){
        this.page=1;
        this.size=10;
    }
    public Pageable getPageable(Sort sort){
        return PageRequest.of(page -1, size, sort);
        //정적으로 생성하기 위해 of를 사용하고, JPA에서 페이지는 0부터 시작하므로 page-1로 전달;
    }
}

 

package com.board.boot3.dto;

import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

//화면에 필요한 결과 목록
@Data

//DTO -> Entity로 변환하기 때문에
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;


    //function 패키지에 있는 람다식으로 사용할 수 있는 편리한 함수형 메서드 :
    //매개변수와 반환값 존재 여부에 따라 구분 / 조건식 표현해 참 거짓 반환
    // Runnable -> run(), Supplier -> get(), Consumer -> accept(), Function -> apply() , Predicate -> test()

    //Page<Entity>의 객체들을 DTO객체로 변환해서 담는 메서드
    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn){
        //페이징 결과를 스트림에 담아서, fn을 이용해 람다식을 통해 변환하고, 최종연산으로 리스트로 반환
        dtoList = result.stream().map(fn).collect(Collectors.toList());

        totalPage = result.getTotalPages();

        makePageList(result.getPageable());
    }//Function 함수형 인터페이스를 사용하면, 제네릭으로 정의되어 있기 때문에 어떤 엔티티를 전달해도 재사용 가능


    private void makePageList(Pageable pageable){

        this.page = pageable.getPageNumber() + 1; // JPA가 전달한 페이지는 0부터 시작하므로 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;


        //페이지 리스트
        //기본형 int스트림을 스트림으로 변환하는 메서드 : mapToObj()와 boxed()
        //int스트림을 이용해 범위 제한해 데이터를 추출하고, 다시 Stream<Integer>로 변환 후, 최종연산으로 담아서 리스트로 변환
        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
        //pageList는 List<Integer>
    }

}

#게시물 등록

 

6. 게시물 서비스 작성

: 인터페이스에 등록 추상메서드 작성 후, 구현 클래스에서 작성

또한, 디폴트 메서드로 dtoToEntity, entityToDto 작성

public interface BoardService {

    Long register(BoardDTO dto);

    //디폴트 메서드
    //DTO -> 엔티티 변환
    default Board dtoToEntity(BoardDTO dto){

        Member member = Member.builder().email(dto.getWriterEmail()).build();

        Board board = Board.builder()
                .bno(dto.getBno())
                .title(dto.getTitle())
                .content(dto.getContent())
                .writer(member)
                .build();
        return board;
    }
    
    //엔티티 -> 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())
                .writerEmail(member.getEmail())
                .writerName(member.getName())
                .replyCount(replyCount.intValue()) //int로 처리하도록
                .build();

        return boardDTO;

    }

 

@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;


    @Override
    public Long register(BoardDTO dto) {

        log.info(dto);

        Board board  = dtoToEntity(dto);

        repository.save(board);

        return board.getBno();
    }

-> 등록시 dtoToEntity 호출 후, 리포지토리에 저장

-> 게시물 번호 반환

 

게시물 등록 테스트

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {

        BoardDTO dto = BoardDTO.builder()
                .title("Test.")
                .content("Test...")
                .writerEmail("user55@aaa.com")  //현재 데이터베이스에 존재하는 회원 이메일
                .build();

        Long bno = boardService.register(dto);

    }

 

게시물 페이징 처리위한 서비스 추상메서드 작성

public interface BoardService {

    Long register(BoardDTO dto);
    PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO);
}

 

@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;

    //JPQL 반환 결과인 Object[]를 BoardDTO로 변환
    //-> entityToDTO() 호출 후, Function 함수형 인터페이스를 이용
    //즉, PageRequestDTO에서 받은 엔티티를 PageResultDTO로 전달하기 위해
    //    PageResultDTO<> 인스턴스를 반환하는 getList() 구현
    @Override
    public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {

        log.info(pageRequestDTO);
        //일반적인 함수형태임으로 자주쓰이는 함수형 인터페이스: 하나의 매개변수를 받아 결과를 반환
        //Object[]을 받아서 apply()메서드에 BoardDTO를 파라미터로 전달해 결과를 반환하는 형태
        //메서드가 하나뿐인 인터페이스이므로 메서드명을 생략해서 사용 -> entityToDTO(Board board, Member member, Long replyCount)
        Function<Object[], BoardDTO> fn = (en -> entityToDTO((Board)en[0],(Member)en[1],(Long)en[2]));

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


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

 

게시물 목록의 페이징 처리 테스트

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;

	//게시물 목록 페이징 처리 테스트
    //요청 받은 목록을 getList를 통해 페이징, 정렬, entityToDto 수행한 목록 처리 테스트
    @Test
    public void testList() {

        //1페이지 10개
        PageRequestDTO pageRequestDTO = new PageRequestDTO();

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

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

    }

 

# 게시물 조회

7. 조회를 위한 추상 메서드 선언과 구현

public interface BoardService {

    Long register(BoardDTO dto);

    PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO);

    BoardDTO get(Long bno);
}

 

@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;
    //게시물 조회
    @Override
    public BoardDTO get(Long bno) {

        Object result = repository.getBoardByBno(bno);

        Object[] arr = (Object[])result;

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

 

게시물 조회 테스트

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;
	
    //게시물 조회 테스트
    @Test
    public void testGet() {

        Long bno = 100L;

        BoardDTO boardDTO = boardService.get(bno);

        System.out.println(boardDTO);
    }
}

 

#게시물 삭제

public interface BoardService {
void removeWithReplies(Long bno);
}
@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;

    private final ReplyRepository replyRepository;
    //게시물 삭제 메서드 구현
    //-> 댓글 삭제 후 게시물 삭제를 한 트랜잭션으로
    @Transactional
    @Override
    public void removeWithReplies(Long bno) {

        //댓글 부터 삭제
        replyRepository.deleteByBno(bno);

        repository.deleteById(bno);

    }

 

게시물 삭제 테스트

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;

    //게시물 삭제 테스트
    @Test
    public void testRemove() {

        Long bno = 1L;

        boardService.removeWithReplies(bno);

    }

 

#게시물 수정

: 게시물 엔티티에 수정을 위한 메서드 추가하고, 서비스에서도 메서드 선언 및 구현

package com.board.boot3.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
//연관관계 테이블의 객체까지 모두 출력하므로, exclude처리
@ToString(exclude = "writer")
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    private String title;

    private String content;

    //일대일 관계, 다대일 관계는 글로벌 패치 전략을 지연으로 설정
    @ManyToOne (fetch = FetchType.LAZY)
    private Member writer;

    public void changeTitle(String title){
        this.title = title;
    }

    public void changeContent(String content){
        this.content = content;
    }
}

 

public interface BoardService {

   void modify(BoardDTO boardDTO);
}
@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;

    private final ReplyRepository replyRepository;
 
    //게시물 수정 메서드
    @Transactional
    @Override
    public void modify(BoardDTO boardDTO) {

        Board board = repository.getOne(boardDTO.getBno());

        if(board != null) {

            board.changeTitle(boardDTO.getTitle());
            board.changeContent(boardDTO.getContent());

            repository.save(board);
        }
    }
 }

 

게시물 수정 테스트

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;
    
    
    //게시물 수정 테스트
    @Test
    public void testModify() {

        BoardDTO boardDTO = BoardDTO.builder()
                .bno(2L)
                .title("제목 변경합니다.2")
                .content("내용 변경합니다.2")
                .build();

        boardService.modify(boardDTO);

    }
}

 

 

더보기
package com.board.boot3.repository;


import com.board.boot3.entity.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface BoardRepository extends JpaRepository<Board, Long> {

    //연관관계가 있는 글과 작성자 정보 조회 메서드
    @Query("select b, w from Board b left join b.writer w where b.bno =:bno")
    //한 개의 로우(Object) 내에 Object[]로 나옴
    Object getBoardWithWriter(@Param("bno") Long bno);
    //Board를 사용하지만, Member를 함께 조회 -> 따라서 b.writer로 사용
    //-> 내부의 엔티티 사용시에는 ON을 붙이지 않는다.

    //연관관계가 없는, 글과 댓글 정보 조회 메서드
    @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);



    //목록 화면 출력을 위한 메서드 : 오라클에서 발생하는 GROUP BY 오류 -> group by 하나씩 적용하거나 서브쿼리
   @Query(value ="SELECT min(r.board),min(b.writer), count(r) "+
            " FROM Board b" +
            " LEFT JOIN b.writer w " +
            " LEFT JOIN Reply r ON r.board = b "+
            "GROUP BY b",
            countQuery ="SELECT count(b) FROM Board b")
    Page<Object[]> getBoardWithReplyCount(Pageable pageable);



    //단일 조회
   @Query(value="SELECT min(r.board), min(b.writer), count(r) " +
            " FROM Board b LEFT JOIN b.writer w " +
            " LEFT OUTER JOIN Reply r ON r.board = b" +
            " Group by b" +
            " having b.bno = :bno",
            countQuery ="SELECT count(b) FROM Board b")
    Object getBoardByBno(@Param("bno") Long bno);


}
package com.board.boot3.service;


import com.board.boot3.dto.BoardDTO;
import com.board.boot3.dto.PageRequestDTO;
import com.board.boot3.dto.PageResultDTO;
import com.board.boot3.entity.Board;
import com.board.boot3.entity.Member;

public interface BoardService {

    Long register(BoardDTO dto);

    PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO);

    BoardDTO get(Long bno);

    void removeWithReplies(Long bno);

    void modify(BoardDTO boardDTO);

    //디폴트 메서드
    //DTO -> 엔티티 변환
    default Board dtoToEntity(BoardDTO dto){

        Member member = Member.builder().email(dto.getWriterEmail()).build();

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

    //엔티티 -> 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())
                .writerEmail(member.getEmail())
                .writerName(member.getName())
                .replyCount(replyCount.intValue()) //int로 처리하도록
                .build();

        return boardDTO;

    }
}
package com.board.boot3.service;


import com.board.boot3.dto.BoardDTO;
import com.board.boot3.dto.PageRequestDTO;
import com.board.boot3.dto.PageResultDTO;
import com.board.boot3.entity.Board;
import com.board.boot3.entity.Member;
import com.board.boot3.repository.BoardRepository;
import com.board.boot3.repository.ReplyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.function.Function;

@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService{

    private final BoardRepository repository;

    private final ReplyRepository replyRepository;



    @Override
    public Long register(BoardDTO dto) {

        log.info(dto);

        Board board  = dtoToEntity(dto);

        repository.save(board);

        return board.getBno();
    }

    //JPQL 반환 결과인 Object[]를 BoardDTO로 변환
    //-> entityToDTO() 호출 후, Function 함수형 인터페이스를 이용
    //즉, PageRequestDTO에서 받은 엔티티를 PageResultDTO로 전달하기 위해
    //    PageResultDTO<> 인스턴스를 반환하는 getList() 구현
    @Override
    public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {

        log.info(pageRequestDTO);
        //일반적인 함수형태임으로 자주쓰이는 함수형 인터페이스: 하나의 매개변수를 받아 결과를 반환
        //Object[]을 받아서 apply()메서드에 BoardDTO를 파라미터로 전달해 결과를 반환하는 형태
        //메서드가 하나뿐인 인터페이스이므로 메서드명을 생략해서 사용 -> entityToDTO(Board board, Member member, Long replyCount)

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

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

//    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn) -> PageResultDTO 객체 구성
        return new PageResultDTO<>(result, fn);
    }

    //게시물 조회
    @Override
    public BoardDTO get(Long bno) {

        Object result = repository.getBoardByBno(bno);

        Object[] arr = (Object[])result;

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

    //게시물 삭제 메서드 구현
    @Transactional
    @Override
    public void removeWithReplies(Long bno) {

        //댓글 부터 삭제
        replyRepository.deleteByBno(bno);

        repository.deleteById(bno);

    }

    //게시물 수정 메서드
    @Transactional
    @Override
    public void modify(BoardDTO boardDTO) {

        Board board = repository.getOne(boardDTO.getBno());

        if(board != null) {

            board.changeTitle(boardDTO.getTitle());
            board.changeContent(boardDTO.getContent());

            repository.save(board);
        }
    }
}
package com.board.boot3.service;

import com.board.boot3.dto.BoardDTO;
import com.board.boot3.dto.PageRequestDTO;
import com.board.boot3.dto.PageResultDTO;
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.Pageable;
import org.springframework.data.domain.Sort;

@SpringBootTest
public class BoardServiceTests {

    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {

        BoardDTO dto = BoardDTO.builder()
                .title("Test.")
                .content("Test...")
                .writerEmail("user55@aaa.com")  //현재 데이터베이스에 존재하는 회원 이메일
                .build();

        Long bno = boardService.register(dto);

    }

    //게시물 목록 페이징 처리 테스트
    //요청 받은 목록을 getList를 통해 페이징, 정렬, entityToDto 수행한 목록 처리 테스트
    @Test
    public void testList() {

        //1페이지 10개
        PageRequestDTO pageRequestDTO = new PageRequestDTO();

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

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

    }

    //게시물 조회 테스트
    @Test
    public void testGet() {

        Long bno = 100L;

        BoardDTO boardDTO = boardService.get(bno);

        System.out.println(boardDTO);
    }

    //게시물 삭제 테스트
    @Test
    public void testRemove() {

        Long bno = 10L;

        boardService.removeWithReplies(bno);

    }

    //게시물 수정 테스트
    @Test
    public void testModify() {

        BoardDTO boardDTO = BoardDTO.builder()
                .bno(2L)
                .title("제목 변경합니다.2")
                .content("내용 변경합니다.2")
                .build();

        boardService.modify(boardDTO);

    }

//
//    @Test
//    public void testSearch(){
//
//        PageRequestDTO pageRequestDTO = new PageRequestDTO();
//        pageRequestDTO.setPage(1);
//        pageRequestDTO.setSize(10);
//        pageRequestDTO.setType("t");
//        pageRequestDTO.setKeyword("11");
//
//        Pageable pageable = pageRequestDTO.getPageable(Sort.by("bno").descending());
//
//        PageResultDTO<BoardDTO, Object[]> result = boardService.getList(pageRequestDTO);
//
//        for (BoardDTO boardDTO : result.getDtoList()) {
//            System.out.println(boardDTO);
//        }
//    }
//

}

 

 

반응형