JPA를 써서 게시판을 만들어보자.
jpa(Java Persistence API)
ORM(Object-Relational Mapping)
우리가 일반 적으로 알고 있는 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑(연결)한다는 뜻이며, 기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 보면된다.
JPA METHOD
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'org.springframework.boot:spring-boot-starter-web'
// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
- application.properties 설정
spring.application.name=boot_JPA
server.port = 8089
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/bootdb2
spring.datasource.username=springUser
spring.datasource.password=mysql
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.thymeleaf.cache=false
● 로그백을 사용해서 로그를 관리
- log4jdbclog4jdbc.log4j2.properties 만들어서 코드 추가.
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
- logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %-5p {%c} %msg%n</pattern>
</encoder>
</appender>
<logger name="com.ezen.boot_JPA" level="INFO" appender-ref="STDOUT"/>
<logger name="jdbc" level="OFF"/>
<logger name="jdbc.connection" level="OFF"/>
<logger name="jdbc.audit" level="OFF"/>
<logger name="jdbc.sqlonly" level="INFO" appender-ref="STDOUT"/>
<logger name="jdbc.resultsettable" level="INFO" appender-ref="STDOUT"/>
<logger name="org.springframework" level="error"/>
<logger name="org.springframework.jdbc" level="error"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
● BootJpaApplication에 @EnableJpaAuditing을 넣어줘야 JPA를 인식할 수 있음.
@EnableJpaAuditing
@SpringBootApplication
public class BootJpaApplication {
public static void main(String[] args) {
SpringApplication.run(BootJpaApplication.class, args);
}
}
● 스프링에서 했던 레이아웃 , 헤더, 푸터를 가져와서 일단 index 화면을 구성
● entity (테이블) 클래스 패키지 , dto ( 객체) 클래스 패키지 생성하고 컨트롤러부터 repository까지 패키지만 일단 생성
● entity에 Board 구성 자주쓰는 코드들은 따로 base 클래스로 별도로관리해서 extends 함
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends TimeBase{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
private long bno;
@Column(length = 200, nullable = false)
private String title;
@Column(length = 200, nullable = false)
private String writer;
@Column(length = 2000, nullable = false)
private String content;
}
- 등록일 , 수정일은 따로 관리
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class}) // 필수 반드시 지정
@Getter
public class TimeBase {
/* 등록일 수정일만 따로 뺴서 관리하는 슈퍼 테이블 */
@CreatedDate
@Column(name = "reg_at", updatable = false)
private LocalDateTime regAt;
@LastModifiedDate
@Column(name = "mod_at")
private LocalDateTime modAt;
}
● BoardDTO 구성
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardDTO {
private long bno;
private String title;
private String writer;
private String content;
private LocalDateTime regAt, modAt;
}
● Repository는 JpaRepository를 상속받아야 JPA를 인지할 수 있음 JpaRepository 안의 값은 테이블과 , id인데 id의 타입이 기본타입말고 클래스타입으로 들어가야함으로 구성한 DTO와 Entity에서 long을 Long으로 수정해주어야함
/* JpaRepository<테이블, id> */
public interface BoardRepository extends JpaRepository<Board, Long> {}
● 컨트롤러에서 일단 register로 넘어갈 수 있게 GetMapping으로 구현하고
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/board/*")
@Controller
public class BoardController {
private final BoardService boardService;
@GetMapping("/register")
public void register(){
}
}
- register 화면을 구성한다음 실행해서 데이터베이스가 잘 만들어지는지, 화면이 잘나오는지 확인
<div layout:fragment="content" class="container-md">
<div>
<h1>Boot Register Page</h1>
<hr>
<form action="/board/register" method="post">
<div class="mb-3">
<label for="t" class="form-label">Title</label>
<input type="text" class="form-control" id="t" name="title" placeholder="title...">
</div>
<div class="mb-3">
<label for="w" class="form-label">Writer</label>
<input type="text" class="form-control" id="w" name="writer" placeholder="writer...">
</div>
<div class="mb-3">
<label for="c" class="form-label">Content</label>
<textarea class="form-control" id="c" name="content" cols="10" rows="3" placeholder="content..."></textarea>
</div>
<button type="submit" class="btn btn-primary" id="regBtn">register</button>
</form>
</div>
</div>
구성한 Entity에 맞게 테이블이 잘 생성되는걸 확인할 수 있음
● 컨트롤러에서 register에서 post한 입력한 정보를 받아와서 DB에 보내서 insert하고 insert에 성공하면
JPA에서는 row를 리턴하는게아닌 id를 리턴하므로 Long형의 bno로 받음.
@PostMapping("/register")
public String register(BoardDTO boardDTO){
log.info("boardDTO >> {} ", boardDTO);
// insert, update , delete => return 1 row
// JPA insert, update ,delete => return id
Long bno = boardService.insert(boardDTO);
log.info(" >>> insert Ok? >> {} " , bno > 0 ? "Ok" : "Fail");
return "/index";
}
● 서비스에서도 default를 사용하면 메서드를 구현할 수 있음.
화면에서 가져온 DTO를 Board 객체로 변환하고 DB에서 가져온 Board객체를 DTO객체로 변환해주는 메서드를 작성해줌.
public interface BoardService {
// 추상 메서드만 가능한 인터페이스
// 메서드가 default(접근제한자) 구현 가능.
Long insert(BoardDTO boardDTO);
// BoardDTO(class) : bno title writer content regAt modAt
// Board(table) : bno title writer content
// BoardDTO => board 변환
// 화면에서 가져온 BoardDTO 객체를 저장을 위한 Board 객체로 변환
default Board convertDtoToEntity(BoardDTO boardDTO){
return Board.builder()
.bno(boardDTO.getBno())
.title(boardDTO.getTitle())
.writer(boardDTO.getWriter())
.content(boardDTO.getContent())
.build();
}
// board => BoardDTO 변환
// DB에서 가져온 Board 객체를 화면에 뿌리기위한 BoardDTO 객체로 변환
default BoardDTO convertEntityToDto(Board board){
return BoardDTO.builder()
.bno(board.getBno())
.title(board.getTitle())
.writer(board.getWriter())
.content(board.getContent())
.regAt(board.getRegAt())
.modAt(board.getModAt())
.build();
}
}
● ServiceImpl로가서 save()를 이용해 insert후 id를 리턴하도록함 save에는 class 객체가아닌 Entity를 파라미터로 전송해야하므로 서비스에서 만든 convert 변환 메서드를 사용해주어야하기에 서비스에서 변환메서드를 필수로 만들어주어야함.
@Override
public Long insert(BoardDTO boardDTO) {
// 저장 객체는 Board
// save() : insert 후 저장 객체의 id를 리턴
// save() Entity 객체를 파라미터로 전송
return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
}
● list를 가져오도록 구현
- 리스트 화면부터 구성해주고
<div class="container-md">
<h1>Board List Page</h1>
<hr>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">no.</th>
<th scope="col">title</th>
<th scope="col">writer</th>
<th scope="col">regAt</th>
</tr>
</thead>
<tbody>
<tr th:each="bvo:${list}">
<td>[[${bvo.bno }]]</td>
<td> <a th:href="@{/board/detail(bno=${bvo.bno})}">[[${bvo.title }]]</a></td>
<td>[[${bvo.writer }]]</td>
<td>[[${bvo.regAt }]]</td>
</tr>
</tbody>
</table>
</div>
- 컨트롤러에서 연결해서 DB에서 리스트를 가져와 model을 이용해서 화면에 전달
@GetMapping("/list")
public void list(Model model){
List<BoardDTO> list = boardService.getList();
model.addAttribute("list",list);
}
- 서비스 구현체에서 구현해주는데 DB에서 Board의 리스트로 리턴하므로 BoardDTO의 리스트로 변환해주어야함
steam().map을 사용해서 변환해서 List안에 넣어주고 리턴
@Override
public List<BoardDTO> getList() {
// 컨트롤러로 보내야하는 리턴은 List<BoardDTO>
// DB에서 가져오는 리턴은 List<Board> -> BoardDTO 객체로 변환
// findAll
// 정렬 : Sort.by(Sort.Direction.DESC, "정렬기준 칼럼명")
List<Board> boardList = boardRepository.findAll(Sort.by(Sort.Direction.DESC, "bno"));
List<BoardDTO> boardDTOList = boardList.stream()
.map(b -> convertEntityToDto(b)).toList();
return boardDTOList;
}
● detail 페이지 구현
- js로 detail 페이지에서 상태변화로 modify까지 구현할 것이므로 form안에 넣어 화면구성
스크립트도 넣어두고 js 파일도 생성.
<div class="container-md">
<h1>Board Detail Page [[${boardDTO.bno}]]</h1>
<hr>
<form action="/board/modify" method="post" id="modForm">
<input type="hidden" name="bno" th:value="${boardDTO.bno}">
<div class="mb-3">
<label for="t" class="form-label">title</label>
<input type="text"
class="form-control" id="t" name="title" th:value="${boardDTO.title }"
readonly="readonly">
</div>
<div class="mb-3">
<label for="w" class="form-label">writer</label>
<input type="text"
class="form-control" id="w" name="writer" th:value="${boardDTO.writer }"
readonly="readonly"> <span class="badge text-bg-info">[[${boardDTO.regAt }]]</span>
<br> <span class="badge text-bg-info">[[${boardDTO.modAt }]]</span>
</div>
<div class="mb-3">
<label for="c" class="form-label">content</label>
<textarea class="form-control" id="c" rows="3" name="content"
readonly="readonly">[[${boardDTO.content }]]</textarea>
</div>
<button type="button" id="listBtn" class="btn btn-info">List</button>
<button type="button" id="modBtn" class="btn btn-primary">Modify</button>
<a th:href="@{/board/delete(bno=${boardDTO.bno})}"><button type="button" id="delBtn" class="btn btn-danger">Delete</button></a>
</form>
</div>
<script th:src="@{/js/boardDetail.js}"></script>
- 컨트롤러에서 받아온 bno를 이용해서 DB에서 가져와서 model을 이용해서 boardDTO를 화면에 보낼 수 있도록함.
@GetMapping("/detail")
public void detail(Model model, @RequestParam("bno") Long bno){
BoardDTO boardDTO = boardService.getDetail(bno);
model.addAttribute("boardDTO" , boardDTO);
}
- findById의 리턴타입 Optional<Board> 타입으로 리턴
◉ Optional<T> : nullPointException 발생하지 않도록 도와줌.
◉ Optional.isEmpty() : null일 경우 확인가능 true / false
◉ Optional.isPresent() : 값이 있는지 확인 true / false
◉ Optional.get() : 객체 가져오기
@Override
public BoardDTO getDetail(Long bno) {
/* findById : 아이디(PK)를 주고 해당 객체를 리턴 */
Optional<Board> optional = boardRepository.findById(bno);
if(optional.isPresent()){
BoardDTO boardDTO = convertEntityToDto(optional.get());
return boardDTO;
}
return null;
}
DB에서 Board로 가져온 optional이 존재하면 객체를 가져와서 DTO로 변환해서 저장한 후 리턴
● modify 페이지 구현
- js 파일에 버튼을 눌렀을때 버튼이 바뀌고 수정이가능하도록 readOnly가 false되도록 구현
document.getElementById('listBtn').addEventListener('click', () => {
location.href = "/board/list";
})
document.getElementById('modBtn').addEventListener('click' , () => {
document.getElementById('t').readOnly = false;
document.getElementById('c').readOnly = false;
// 버튼 생성
let modBtn = document.createElement("button");
modBtn.setAttribute('type','submit');
modBtn.classList.add('btn', 'btn-outline-primary');
modBtn.innerText="submit";
// 추가
document.getElementById('modForm').appendChild(modBtn);
document.getElementById("modBtn").remove();
document.getElementById("delBtn").remove();
})
- 컨트롤러에서 DB에 화면에서 수정된 내용보내고 update 할 수 있도록 구현.
@PostMapping("/modify")
public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
Long bno = boardService.modify(boardDTO);
redirectAttributes.addAttribute("bno",boardDTO.getBno());
return "redirect:/board/detail";
}
- update는 따로없으니 save를 이용 수정내용을 set해주고 save해주어야함.
@Override
public Long modify(BoardDTO boardDTO) {
// update : jpa는 업데이트가 없음
// 기존 객체를 가져와서 set 수정 후 다시 저장
Optional<Board> optional = boardRepository.findById(boardDTO.getBno());
if(optional.isPresent()){
Board entity = optional.get();
entity.setTitle(boardDTO.getTitle());
entity.setContent(boardDTO.getContent());
return boardRepository.save(entity).getBno();
}
return null;
}
● delete 구현
- 버튼에 a태그달아서 delete 경로 적어주고 bno를 보내줌
<a th:href="@{/board/delete(bno=${boardDTO.bno})}"><button type="button" id="delBtn" class="btn btn-danger">Delete</button></a>
- 컨트롤러에서 받아온 bno로 삭제 서비스 연결
@GetMapping("/delete")
public String delete(@RequestParam("bno") Long bno){
boardService.delete(bno);
return "redirect:/board/list";
}
- deleteById로 받아온 bno로 삭제 void이므로 리턴없음.
// 삭제 : deleteById(id)
@Override
public void delete(Long bno) {
boardRepository.deleteById(bno);
}
'boot_JPA 수업 정리' 카테고리의 다른 글
5일 (0) | 2024.11.26 |
---|---|
Boot_JPA 4일차 (1) | 2024.11.25 |
Boot_JPA 3일차 (0) | 2024.11.22 |
Boot_JPA 2일차 (3) | 2024.11.21 |