PROJECT #4 - JPA

김형우·2022년 4월 11일
0

New Project

목록 보기
5/12

0. 설치

  • pom.xml
  • hibernate 5.6.7버전에 Containing 오류가 있어서 다운그레이드 후 사용한다
		<!-- JPA 라이브러리 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- hibernate 다운그레이드 -->
		<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
  1. create : 서버구동시 테이블 생성, 재구동시 또 생성
    : 데이터 초기화 되어버림.
  2. create-drop : 삭제되는 시점이 서버가 종료 되는 시점
    : 데이터 초기화 되어버림.
  3. update : DDL에 변화가 생길때만 생성,
    : 데이터는 유지됨.
  4. 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>
        <!-- <p th:text="${_csrf.token}"></p> -->
    </div>  
    <script>
        function updateHit(no) {
            // location.href = "/ROOT/board/selectone?no="+no;
            // alert(no);
            // 1. 조회수 증가용 restful 호출
            // axios 같은것
            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){
                    // 2. 해당 페이지로 이동
                    location.href = "/ROOT/board/selectone?no="+no;
                }
            }
            // 호출해야 onload가 반응
            xhr.send();
        }
    </script>
    <!-- <script>
        function handleUpdate(no) {
            location.href="/ROOT/seller/update?code="+no;
        }
        function handleDelete(no) {
            if(confirm('삭제할까요?')){
                console.log(no);
                // GET으로 삭제처리 주소창을 바꿈
                // location.href="/ROOT/seller/delete?code="+no;

                // POST로 처리
                // <form th:action="@{/seller/delete}" method = "post">
                var form = document.createElement("form");
                form.method = "post";
                form.action = "/ROOT/seller/delete"
                
                // <input type="text" name="code" value=no />
                var input = document.createElement("input");
                input.name = "code";
                input.value = no;

                // Security의 _csrf의 token값 전송
                // <input type="hidden" name="_csrf" value="0eb9f953-68cb-4fab-9f7c-32c6edb79924"/>
                var input1 = document.createElement("input");
                input1.type = "hidden";
                input1.name = "_csrf";
                input1.value = '[[${_csrf.token}]]';
                console.log(input1);
                
                // input type을 form태그에 추가
                form.appendChild(input);
                form.appendChild(input1);
                // form document에 추가
                document.body.appendChild(form);
                // <input type="submit">을 누름
                form.submit();
            }
        }
    </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>
<!-- no=17, title=dhg,, content=sfg, writer=sfitrs, hit=1, regdate=2022-04-11 14:18:50.064 -->
<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>

        <!-- 이전글 다음글 -->
        <!-- https://docs.spring.io/spring-data/jpa/docs/2.6.3/reference/html/#repositories.limit-query-result -->
        
    </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){
                    // 2. 해당 페이지로 이동
                    location.href = "/ROOT/board/selectone?no="+e.target.response.prevno;
                }
            }
            // 호출해야 onload가 반응
            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){
                    // 2. 해당 페이지로 이동
                    location.href = "/ROOT/board/selectone?no="+e.target.response.nextno;
                }
            }
            // 호출해야 onload가 반응
            xhr.send();
        }
    </script> 
</body>
</html>
profile
The best

0개의 댓글