0. 설치
- pom.xml
- hibernate 5.6.7버전에 Containing 오류가 있어서 다운그레이드 후 사용한다
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.4.Final</version>
</dependency>
1. 환경설정
- 마이바티스는 dto라고 부르고
dto는 매퍼에서 명령을 수행한 결과를 담는 용도
담겨야하는 변수, 타입만 정의
- JPA는 ENTITY라고 부른다
jpa는 좀 더 상세하게 설정한다
상세하게 다함
1-1. application.properties
- JPA 설정!!!!!!!!!!!!!!!!!!!!!!!!
- entity에 정의된 DDL
- create : 서버구동시 테이블 생성, 재구동시 또 생성
: 데이터 초기화 되어버림.
- create-drop : 삭제되는 시점이 서버가 종료 되는 시점
: 데이터 초기화 되어버림.
- update : DDL에 변화가 생길때만 생성,
: 데이터는 유지됨.
- validate : entity와 테이블 타입 또는 컬럼명이 정확한지 확인만
update까지는 괜찮음, validate만 사용할것.
- 나머지는 개발용, 초기화 자주시켜야 하는 경우 ex)데이터 꼬임 : 없는 외래키가 들어온다던가
spring.jpa.hibernate.ddl-auto=update
1-2. Boot20220406Application.java
- mtbatis에서 DTO == jap에서 entity
- mtbatis에서 Mapper == jap에서 repository
// entity 사용 경로 추가
// 엔티티(jpa) == DTO(mybatis)
@EntityScan(basePackages = {
"com.example.entity"
})
// JPA 사용 경로 추가
// 저장소(jpa) == 매퍼(mybatis)
@EnableJpaRepositories(basePackages = {
"com.example.repository"
})
2. 사용
2-1. entity 생성
- 변수명과 컬럼명을 따로 설정 가능
- 컬럼명을 별도로 정의하지 않으면 변수명과 같은 것으로 정의함
package com.example.entity;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;
@Entity
@Data
// 테이블명
@Table(name = "BOARD10")
// 시퀀스 생성
@SequenceGenerator(name = "SEQ_BOARD", // 시퀀스
sequenceName = "SEQ_BOARD10_NO", // 시퀀스명
allocationSize = 1, // 증가값
initialValue = 1) // 시작값
public class BoardEntity {
@Id
@Column(name = "BNO") // 컬럼명
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_BOARD") // 시퀀스 적용
private Long no; // 타입 변수
@Column(name = "BTITLE", length = 200) // VARCHAR2(200)
private String title;
@Lob // CLOB
@Column(name = "BCONTENT")
private String content;
@Column(name = "BWRITER")
private String writer;
@Column(name = "BHIT")
private long hit = 1L;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@CreationTimestamp // CURRENT_DATE
@Column(name = "BREGDATE")
private Date regdate;
}
2-2. 저장소 생성
package com.example.repository;
import java.util.List;
import com.example.entity.BoardEntity;
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 org.springframework.stereotype.Repository;
@Repository
public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
// 마이바티스는 상속없이 Mapper만 정의하고 사용
// JPA는 구현체가 있음 interface로 만들어 놨다
// 기본적인 CRUD 사용 가능
// https://docs.spring.io/spring-data/jpa/docs/2.6.3/reference/html/#jpa.sample-app.finders.strategies
// table3 에서 설명해 둔 형식으로 만들어 봄
// 검색기능
// findBy컬럼명Containing
// == WHERE TITLE LIKE '%'||'검색어'||'%'
List<BoardEntity> findByTitleContaining(String title);
// 검색기능 + 정렬
// findBy컬럼명ContainingOrderBy컬럼명Desc
// == WHERE TITLE LIKE '%'||'검색어'||'%' ORDER BY NO DESC
List<BoardEntity> findByTitleContainsOrderByNoDesc(String title);
// 페이지네이션용 게시물 개수
// SELECT COUNT(*) FROM BOARD10
// WHERE TITLE LIKE '%'||'검색어'||'%' ORDER BY NO DESC;
long countByTitleContaining(String title);
// 검색+페이지네이션+정렬
// SELECT B.*, ROW_NUMBER() OVER( ORDER BY BNO DESC ) FROM BOARD10 B
// WHERE BTITLE LIKE '%' || '검색어' || '%'
List<BoardEntity> findByTitleContainsOrderByNoDesc(String title, Pageable pageable);
// 이전글 ex) 20이면 작은것중 가장 큰 값 = 19, 18, 17, 16
BoardEntity findTop1ByNoLessThanOrderByNoDesc(long no);
BoardEntity findFirstByNoLessThanOrderByNoDesc(long no);
// 다음글 ex) 20이면 큰것중 가장 작은 값 = 21, 22, 23, 24
BoardEntity findTop1ByNoGreaterThanOrderByNoAsc(long no);
BoardEntity findFirstByNoGreaterThanOrderByNoAsc(long no);
// 검색기능 query문
@Query(value = "SELECT * FROM BOARD10 WHERE BTITLE LIKE %?1% ", nativeQuery = true)
List<BoardEntity> selectBoardList1(String title);
@Query(value = " SELECT * FROM BOARD10 B WHERE BTITLE LIKE %:ti% ", nativeQuery = true)
List<BoardEntity> selectBoardList(
@Param(value = "ti") String title);
}
- JpaRepository<BoardEntity, Long>
- 구현체 (이미 만들어진 기본적인 CRUD)
2-3. Controller 생성
- BoardController.java
- save(entity객체) == INSERT INTO
package com.example.controller;
import java.util.List;
import com.example.entity.BoardEntity;
import com.example.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping(value = "/board")
public class BoardController {
// global.properties 사용
@Value("${board.page.count}")
int PAGECNT;
@Autowired
BoardRepository bRepository;
// 추가 GET
@GetMapping(value = { "/insert" })
public String insertGET() {
return "board/insert";
}
// 추가 POST
@PostMapping(value = "/insert")
public String insertPOST(
@ModelAttribute BoardEntity board) {
// save(entity객체) == INSERT INTO
bRepository.save(board);
return "redirect:/board/insert";
}
// 목록 GET
@GetMapping(value = "/selectlist")
public String selectlistGET(Model model,
@RequestParam(value = "txt", defaultValue = "") String title,
@RequestParam(value = "page", defaultValue = "1") int page) {
// 페이지네이션
// (시작페이지(0부터), 개수)
PageRequest pageRequest = PageRequest.of(page - 1, PAGECNT);
// findAll == SELECT * FROM BOARD10
// List<BoardEntity> list = bRepository.selectBoardList(title);
List<BoardEntity> list = bRepository.findByTitleContainsOrderByNoDesc(title, pageRequest);
model.addAttribute("list", list);
// 페이지개수
long total = bRepository.countByTitleContaining(title);
model.addAttribute("pages", (total - 1) / PAGECNT + 1);
// System.out.println("=== list === " + list);
return "board/selectlist";
}
// 상세 GET
@GetMapping(value = "/selectone")
public String selectoneGET(Model model,
@RequestParam(name = "no") long no) {
BoardEntity board = bRepository.findById(no).orElse(null);
model.addAttribute("board", board);
return "board/selectone";
}
}
2-4. RESTController 사용
package com.example.restcontroller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.example.dto.BoardDTO;
import com.example.entity.BoardEntity;
import com.example.mapper.BoardMapper;
import com.example.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/board")
public class BoardRestController {
// mybatis
@Autowired
BoardMapper bMapper;
// jpa
@Autowired
BoardRepository bRepository;
// 이전글
// 127.0.0.1:9090/ROOT/api/board/prev?no=14
@RequestMapping(value = "/prev", method = { RequestMethod.GET }, consumes = {
MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardPrevGET(
Model model,
@RequestParam(name = "no") long no) {
Map<String, Object> map = new HashMap<>();
BoardEntity board = bRepository.findFirstByNoLessThanOrderByNoDesc(no);
// System.out.println("=== board === " + board);
// map.put("board", board);
map.put("prevno", board.getNo());
map.put("status", 200);
System.out.println("=== map === " + map);
return map;
}
// 다음글
// 127.0.0.1:9090/ROOT/api/board/next?no=14
@RequestMapping(value = "/next", method = { RequestMethod.GET }, consumes = {
MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardNextGET(
Model model,
@RequestParam(name = "no") long no) {
Map<String, Object> map = new HashMap<>();
BoardEntity board = bRepository.findTop1ByNoGreaterThanOrderByNoAsc(no);
// System.out.println("=== board === " + board);
// map.put("board", board);
map.put("nextno", board.getNo());
map.put("status", 200);
return map;
}
// 조회수 1증가 rest로 만들고, 호출함
// 127.0.0.1:9090/ROOT/api/board/updatehit1?bno=6
@RequestMapping(value = "/updatehit1", method = { RequestMethod.PUT }, consumes = {
MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardUpdateHit1PUT(@RequestParam(name = "no") long no) {
Map<String, Object> map = new HashMap<>();
try {
BoardEntity board = bRepository.findById(no).orElse(null);
board.setHit(board.getHit() + 1L);
bRepository.save(board);
map.put("status", 200);
// map.put("board", board);
} catch (Exception e) {
map.put("status", 0);
}
return map;
}
// global.properties 사용
@Value("${board.page.count}")
int PAGECNT;
// 조회수 1 증가
// 127.0.0.1:9090/ROOT/api/board/updatehit?bno=6
@RequestMapping(value = "/updatehit", method = { RequestMethod.PUT }, consumes = {
MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardUpdateHitPUT(@RequestParam(name = "bno") long bno) {
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
int ret = bMapper.updateBoardHit(bno);
if (ret == 1) {
map.put("status", 200);
}
return map;
}
// 게시판 글 조회
// 127.0.0.1:9090/ROOT/api/board/select?page=1
@RequestMapping(value = "/select", method = { RequestMethod.GET }, consumes = { MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardSelectGET(@RequestParam(name = "page", defaultValue = "1") long page) {
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
long start = (page * PAGECNT) - (PAGECNT - 1);
long end = page * PAGECNT;
List<BoardDTO> list = bMapper.selectBoardList(start, end);
System.out.println("=== list === " + list);
if (list != null) {
map.put("result", list);
map.put("status", 200);
}
return map;
}
// 게시글 한개 조회
// 127.0.0.1:9090/ROOT/api/board/selectone?bno=6
@RequestMapping(value = "/selectone", method = { RequestMethod.GET }, consumes = {
MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardSelectOneGET(@RequestParam(name = "bno") long bno) {
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
BoardDTO retBoard = bMapper.selectBoardOne(bno);
System.out.println("=== retBoard === " + retBoard);
if (retBoard != null) {
boardUpdateHitPUT(retBoard.getBno());
map.put("status", 200);
map.put("result", retBoard);
}
return map;
}
// 게시판글수정
// 127.0.0.1:9090/ROOT/api/board/update
@RequestMapping(value = "/update", method = { RequestMethod.PUT }, consumes = { MediaType.ALL_VALUE }, produces = {
MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardUpdatePUT(@RequestBody BoardDTO board) {
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
int ret = bMapper.updateBoardOne(board);
if (ret == 1) {
map.put("status", 200);
}
return map;
}
// 게시판글쓰기
// 127.0.0.1:9090/ROOT/api/board/insert
@RequestMapping(value = "/insert", // 주소
method = { RequestMethod.POST }, // method 타입
consumes = { MediaType.ALL_VALUE }, // 받는타입
produces = { MediaType.APPLICATION_JSON_VALUE } // 반환타입
)
public Map<String, Object> boardInsertPOST(
@RequestBody BoardDTO board) {
int ret = bMapper.insertBoardOne(board);
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
if (ret == 1) {
map.put("status", 200);
}
return map;
}
// 게시글 삭제
// 127.0.0.1:9090/ROOT/api/board/delete
@RequestMapping(value = "/delete", method = { RequestMethod.DELETE }, consumes = {
MediaType.ALL_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
public Map<String, Object> boardDeletePOST(@RequestParam(name = "bno") long bno) {
Map<String, Object> map = new HashMap<>();
map.put("status", 0);
int ret = bMapper.deleteBoardOne(bno);
if (ret == 1) {
map.put("status", 200);
}
return map;
}
}
3. VIEW
3-1. board/insert.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시글등록</title>
</head>
<body>
<div>
<h3>게시글등록</h3>
<hr>
<a th:href="@{/home}">홈</a>
<form th:action="@{/board/insert}" method="post">
제목 : <input type="text" name="title" /><br />
내용 : <input type="text" name="content" /><br />
작성자 : <input type="text" name="writer" /><br />
<br />
<input type="submit" value="글쓰기" />
</form>
</div>
</body>
</html>
3-2. board/selectlist.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판</title>
</head>
<body>
<div>
<h3>게시판</h3>
<hr>
<a th:href="@{/home}">홈</a>
<a th:href="@{/board/insert}">글쓰기</a>
<a th:href="@{/board/selectlist}">게시판목록</a><br><br>
<br>
<form th:action="@{/board/selectlist}">
<input type="text" name="txt" placeholder="Search...">
<input type="submit" value="검색">
</form>
<table border="1">
<tr>
<th>글번호</th>
<th>제목</th>
<th>내용</th>
<th>작성자</th>
<th>조회수</th>
<th>등록일</th>
<th>버튼</th>
</tr>
<tr th:each="list : ${list}">
<td>
<a th:href="@{/board/selectone(bno=${list.no})}"
th:text="${list.no}"></a>
</td>
<td>
<a href="#" th:onclick="|javascript:updateHit('${list.no}')|" th:text="${list.title}"></a>
</td>
<td th:text="${list.content}"></td>
<td th:text="${list.writer}"></td>
<td th:text="${list.hit}"></td>
<td th:text="${list.regdate}"></td>
<td>
<button th:onclick="|javascript:handleUpdate('${list.no}')|">수정</button>
<button th:onclick="|javascript:handleDelete('${list.no}')|">삭제</button>
</td>
</tr>
</table>
<th:block th:each="i : ${#numbers.sequence(1, pages)}">
<a th:href="@{/board/selectlist(page=${i}, txt=${param.txt})}"
th:text="${i}"></a>
</th:block>
</div>
<script>
function updateHit(no) {
console.log("=== no === "+ no);
const xhr = new XMLHttpRequest();
console.log("=== xhr === " + xhr);
const url = "/ROOT/api/board/updatehit1?no="+no;
console.log("=== url === " + url);
xhr.open("PUT", url, true);
xhr.responseType = "json";
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8;');
xhr.onload = function(e) {
console.log("=== e.target === %j" , e.target);
if(e.target.response.status === 200){
location.href = "/ROOT/board/selectone?no="+no;
}
}
xhr.send();
}
</script>
</body>
</html>
3-3. board/selectone.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판상세</title>
</head>
<body>
<div>
<h3>게시판상세</h3>
<hr>
<a th:href="@{/home}">홈</a>
<a th:href="@{/board/insert}">글쓰기</a>
<a th:href="@{/board/selectlist}">게시판목록</a>
<hr>
글번호 : <span th:text="${board.no}"></span><br />
제목 : <span th:text="${board.title}"></span><br />
내용 : <span th:text="${board.content}"></span><br />
작성자 : <span th:text="${board.writer}"></span><br />
조회수 : <span th:text="${board.hit}"></span><br />
등록일 : <span th:text="${board.regdate}"></span><br />
<button>수정</button>
<button>삭제</button>
<a href="#" th:onclick="|javascript:handlePrev('${board.no}')|"><button>이전글</button></a>
<a href="#" th:onclick="|javascript:handleNext('${board.no}')|"><button>다음글</button></a>
<a th:href="@{/board/selectlist}"><button>목록</button></a>
</div>
<script>
function handlePrev(no) {
console.log("=== no === ",no);
const xhr = new XMLHttpRequest();
console.log("=== xhr === " + xhr);
const url = "/ROOT/api/board/prev?no="+no;
console.log("=== url === " + url);
xhr.open("GET", url, true);
xhr.responseType = "json";
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8;');
xhr.onload = function(e) {
console.log("=== e.target === %j" , e.target);
if(e.target.response.status === 200){
location.href = "/ROOT/board/selectone?no="+e.target.response.prevno;
}
}
xhr.send();
}
function handleNext(no) {
console.log("=== no === ",no);
const xhr = new XMLHttpRequest();
console.log("=== xhr === " + xhr);
const url = "/ROOT/api/board/next?no="+no;
console.log("=== url === " + url);
xhr.open("GET", url, true);
xhr.responseType = "json";
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8;');
xhr.onload = function(e) {
console.log("=== e.target === %j" , e.target);
if(e.target.response.status === 200){
location.href = "/ROOT/board/selectone?no="+e.target.response.nextno;
}
}
xhr.send();
}
</script>
</body>
</html>