요구사항
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);
// }
// }
//
}
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 댓글 프로젝트] 3-4. 댓글 비동기처리를 위한 @RestController와 JSON 처리 (1) | 2022.10.04 |
---|---|
[Spring 부트 - 댓글 프로젝트] 3-2. 게시물과 댓글, 컨트롤러와 화면 처리 [자바스크립트] (1) | 2022.10.03 |
[Spring 부트 - 방명록 미니 프로젝트] 2-3. 서비스, DTO, 컨트롤러 작성 (2) (0) | 2022.10.02 |
[Spring 부트 - 방명록 미니 프로젝트] 2-2. 서비스, DTO, 컨트롤러 작성 (1) (0) | 2022.10.02 |
[Spring 부트 - 방명록 미니 프로젝트] 2-1. 프로젝트 생성과 Querydsl (0) | 2022.10.02 |