20221006 [Spring Boot, JPA]

Yeoonnii·2022년 10월 10일
0

TIL

목록 보기
42/52
post-thumbnail

Spring Data JPA

Spring Data JPA는 Spring에서 제공하는 모듈 중 하나로, 개발자가 JPA를 더 쉽고 편하게 사용할 수 있도록 도와준다. JPA를 한 단계 추상화시킨 Repository라는 인터페이스를 제공한다

  • CRUD 처리를 위한 공통 인터페이스 제공
  • repository 개발 시 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입
  • 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있도록 지원

springdocs 참고하여 Query Methods 사용하기

Query Methods = 쿼리문과 같은 역할
springdocs Query Methods 링크

entity / Member.java

테이블 생성

@Entity
@Data
@Table(name = "MEMBERTBL")
public class Member {
    @Id
    @Column(length = 30)
    String userid;
    
    @Column(length = 200)
    String userpw;
    
    int age;

    @Column(length = 15)
    String phone;
    
    @Column(length = 1)
    String gender;
    
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm.ss.SSS")
    @CreationTimestamp
    // updatable => 수정시에도 날짜 갱신/변경여부
    @Column(name = "REGDATE", updatable = false)
    private Date regdate = null;
    
    @Column(length = 20)
    String role;
    
    @Column(length = 1)
    int block;

    @JsonBackReference(value = "member1")
    @OneToMany(mappedBy = "member") 
    List<Item> item;
}

entity / Item.java

테이블 생성

@Entity
@Data
@Table(name = "ITEMTBL")
@SequenceGenerator(name = "SEQ_ITEM_NO", sequenceName = "SEQ_ITEM_NO", initialValue = 1, allocationSize = 1)
public class Item {
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ITEM_NO")
    @Id
    long no;

    @Column(length = 100)
    String name;

    @Lob
    String content;

    long price;

    long quantity;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm.ss.SSS")
    @CreationTimestamp
    // updatable => 수정시에도 날짜 갱신/변경여부
    @Column(name = "REGDATE", updatable = false)
    private Date regdate = null;

    // 외래키 설정
    @ToString.Exclude
    @JsonBackReference(value = "member1")
    @ManyToOne
    @JoinColumn(name = "SELLER")
    Member member;
}

ItemRepository.java

JpaRepository 상속받아 사용
➡️ extends JpaRepository<엔티티명, 해당 엔티티의 Id 타입>

Query Methods를 사용하여 조회
➡️ findByFirstnameContaining = 검색결과 조회하기
➡️ findByAgeOrderByLastnameDesc = 검색결과 정렬하여 조회하기

@Repository
public interface ItemRepository extends JpaRepository<Item, Long>{
    // 검색기능 가져오기
    // SELECT i FROM Item i Where i.name LIKE '%' || ?1 || '%'
    // String name에 해당하는 항목만 검색해준다
    List<Item> findByNameContaining(String name);
    
    // 내림차순 정렬
    // SELECT i FROM Item i Where i.name LIKE '%' || ?1 || '%'
    // ORDER BY no DESC
    // List<Item> findByNameContainingOrderByNoDesc(String name); // 이름검색
    List<Item> findByNameContainingOrderByNoDesc(String name);
}

ItemController.java

List<Item> list = iRepository.findByNameContaining("");
➡️ 전체조회
List<Item> list = iRepository.findByNameContaining(name);
➡️ name값에 해당하는 내용만 조회
List<Item> list = iRepository.findByNameContainingOrderByNoDesc(name);
➡️ name값에 해당하는 내용만 내림차순 정렬하여 조회

@GetMapping(value = {"/selectlist.do"})
    public ModelAndView selectlistGET(
    @RequestParam(name = "name") String name
    ){
	// List<Item> list = iRepository.findByNameContaining("");
	// List<Item> list = iRepository.findByNameContaining(name); // => name에 해당하는값만 검색
	List<Item> list = iRepository.findByNameContainingOrderByNoDesc(name); // => name에 해당하는값만 내림차순 정렬하여 검색
		ModelAndView mav = new ModelAndView();
        mav.setViewName("item_selectlist");
        mav.addObject("list", list);
			return mav;
    }

변수 관리하여 사용하기

스프링 구동을 위한 환경변수가 아닌,
프로그램 코드를 작성하며 필요한 변수를 명시해두고 필요한경우 사용한다
➡️ BoardImageController의 default image 와 ItemController의 count를 변수로 명시하여 사용

resource/global.properties

# 페이지네이션 가져올 글 개수의 변수
item.list.size=10
# 기본이미지 변수
default.image=classpath:/static/image/noimage.jpg

Application.java

global.properties 생성 후 application에 파일 등록을 해야 사용할 수 있다

// 환경변수 파일 사용 설정
@PropertySource("global.properties")

➡️ Application등록 후 사용을 원하는 Controller에 가서 명시해준다

ItemController.java

@Controller
@RequestMapping(value = {"/item"})
public class ItemController {

    @Value("${item.list.size}") int SIZE; //size=10
...

// 페이지네이션 설정(0부터, 1페이지에 출력될 개수)
        PageRequest pageRequest = PageRequest.of(page-1, SIZE);
	ModelAndView mav = new ModelAndView();
    ...
	mav.addObject("pages", (count-1)/SIZE +1);
    return mav;

BoardImageController.java

@Controller
@RequestMapping(value = "/boardimage")
public class BoardImageController {
    
    @Value("${default.image}") String DEFAULT_IMAGE;
...
//default이미지 설정시
            //InputStream is = resourceLoader.getResource("classpath:/static/image/noimage.jpg")
            InputStream is = resourceLoader.getResource(DEFAULT_IMAGE)

페이지네이션을 위한 글 개수 세기

ItemRepository.java

검색어에 해당하는 글 개수 세기


    // SELECT COUNT(*) i FROM Item i Where i.name LIKE '%' || ?1 || '%'
    long countByNameContaining(String name);

ItemController.java

전체개수에서 검색결과의 갯수 구하기

        long count = iRepository.countByNameContaining(name);
...
				mav.addObject("count", count);

페이지네이션

ItemController.java

페이지네이션 버튼 생성을 위한 pages의 개수구하기 ➡️ (count-1)/SIZE +1)

@GetMapping(value = {"/selectlist.do"})
    public ModelAndView selectlistGET(
        @RequestParam(name = "name") String name,
        @RequestParam(name = "page", defaultValue = "1") int page){

        // 페이지네이션 설정(0부터, 1페이지에 출력될 개수)
        PageRequest pageRequest = PageRequest.of(page-1, 10);

      //목록
      List<Item> list = iRepository.findByNameContainingOrderByNoDesc(name, pageRequest); // => name에 해당하는값만 내림차순 정렬하여 검색
      
      // 전체개수에서 검색결과의 갯수 구하기
        long count = iRepository.countByNameContaining(name);
				
		ModelAndView mav = new ModelAndView();
        mav.setViewName("item_selectlist");
        mav.addObject("list", list);
        mav.addObject("count", count);
        mav.addObject("pages", (count-1)/SIZE +1);

        return mav;
    }

item_selectlist.html

    <th:block th:each="i : ${#numbers.sequence(1, pages)}">
	<!-- http://127.0.0.1:8080/ROOT/item/selectlist.do?name=&page=1 -->
        <a th:href="@{/item/selectlist.do(name=${param.name} ,page=${i})}" th:text="${i}" ></a>
    </th:block>

👨‍🏫 되도록이면 Native SQL문 사용을 자제하는게 좋다
Native SQL문 사용보다 JPQL 사용을 권장한다

[ 조회 구현방법 우선순위 ]
1. Spring Data JPA findby.. ➡️ Query Methods 사용
2. JPQL ➡️ entity기반으로 짜기
3. Native SQL

Native SQL은 관리하기 쉽지 않고 자주 사용하면 이식성이 떨어진다
1. 가능하면 표준 JPQL 사용권장
2. 그래도 안되면 hibernate 같은 JPA 구현체가 제공하는 기능 사용 권장
3. 그래도 안되면 Native SQL 사용권장
4. 그래도 부족하면 MyBatis나 JdbcTemplate 같은 SQL 매퍼와 JPA를 함께 사용권장


외래키 제약조건이 걸린 데이터 삭제

게시글 삭제시 이미지와 댓글이 존재하지 않는 게시글은 삭제가 가능하지만
게시판 목록의 게시글은 외래키가 걸려있기 때문에 삭제가 불가하다
➡️ BoardImage, BoardReply에 외래키가 걸려있다

외래키가 걸린 게시글을 삭제하려면,
댓글과 이미지를 전부 삭제해야만 해당 게시글을 삭제할 수 있다

💡 Cascade의 옵션을 사용하여 원본 데이터를 삭제하는 경우
외래키를 가진 정보들도 모두 삭제가 가능하다!

Cascade 옵션값 사용하기

Cascade 옵션값은 parent-child 관계 일 때 @OneToMany, @ManyToOne의 옵션 값이며, Entity의 상태가 변화했을 때 관계가 있는 Entity에도 상태 변화를 전파시키는 옵션이다

여러개의 Cascade 타입 중 remove를 이용하면
연관 관계에 있는 영속 인스턴스를 연쇄적으로 삭제한다

CascadeType.REMOVE는 관계가 끊어졌다는 것을 제거로 보지 않기 때문에,
관계가 끊어진 child를 자동 제거하지는 않는다

@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
➡️ cascade 옵션을 추가하면 원본이 지워질때 외래키로 가진 정보들이 모두 지워진다
= 원본데이터를 지우면 해당 외래키의 레퍼런스(=참조값)도 지워진다

entity/Board.java

...

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardReply> reply;

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardImage> image;
    
...

양방향 관계/단방향 관계

두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계,
두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다

지금은 양방향 사용하는중!

Board = BoardImage/BoardReply
One ↔ many 관계를 갖고있다

entity의 필요한 데이터만 가져오기

BoardImage/BoardReply
Board entity의 no를 참조할 때 Board entity를 전부 가져온다
➡️ entity 전체를 가져오는게 아닌 entity에서 필요한 데이터만 가져와서 사용해보기

entity/BoardReply.java

entity/BoardImage.java

referencedColumnName = "NO"를 추가하여,
board entity 전체가 아닌 board의 no만 가져오게 설정

@JoinColumn(name = "BRDNO", referencedColumnName = "NO")
    Board board; 

BoardReplyRepository.java

@Repository
public interface BoardReplyRepository extends JpaRepository<BoardReply, Long>{
 
    // Board의 하위는 _ 언더바로 표시! 
    // Board_no = board의 no
    // 엔티티에는 언더바를 사용한 변수를 이용하면 안된다!
    // 엔티티 하위 위치 표기시 인식이 안될수도 있다
    List<BoardReply> findByBoard_noOrderByNoAsc(long no, Pageable pageable);
}

entity의 필요한 컬럼만 골라 새 entity 생성

BoardImage entity중 일부 컬럼만 가져오기

BoardImageProject.java

BoardImage entity의 항목 중 이미지 번호, 게시글 번호만 가져오기

// enttity의 항목중에서 일부 컬럼만 가져오기
public interface BoardImageProject {
    
    //  Long no;
    Long getNo();

    //  Board board;
    Board getBoard();
}

BoardImageRepository.java

이미지는 이미지 번호만 알 수 있다면,
이미지 url 생성이 가능하기 때문에 모든 이미지 정보가 필요하지 않다

@Repository
public interface BoardImageRepository extends JpaRepository<BoardImage, Long>{

	List<BoardImageProject> findByBoard_noOrderByNoAsc(long no);
}

BoardController.java

biRepository.findByBoard_noOrderByNoAsc(no)를 이용하여 List<BoardImageProject>를 조회

// 게시글 상세페이지로 이동
    @GetMapping(value = "/selectone.do")
    public ModelAndView selectOneGET(@RequestParam(name = "no") Long no){
        Board board = bRepository.findById(no).orElse(null);

        // 댓글목록 확인용 출력 => stackoverflow
        System.out.println("=======board.getReply======");
        System.out.println(board.getReply().toString());
        
        // 이미지 확인용
        System.out.println("=======board.getImage======");
        System.out.println(board.getImage().toString());

        // 페이지네이션 가져오기
        PageRequest pageRequest = PageRequest.of(0,5);
        
        List<BoardReply> reply = brRepository.findByBoard_noOrderByNoAsc(no, pageRequest);
        
        List<BoardImageProject> image = biRepository.findByBoard_noOrderByNoAsc(no);

        ModelAndView mav = new ModelAndView();
        mav.setViewName("board_selectone");
        // 게시글에 해당하는 댓글 목록 list도 board는 이미 갖고 있다
        mav.addObject("brd", board);
        mav.addObject("reply", reply);
        mav.addObject("image", image);
        
        return mav;
    }

board_selectone.html

board 엔티티에 포함되어있는 image정보를 불러오는게 아닌
Controller에서 넘겨준 List<BoardImageProject> image를 받아 이미지url을 생성해준다

<tr th:each="obj, idx : ${brd.image}"><tr th:each="obj, idx : ${image}">
<td><img th:src="@{/boardimage/image(no=${obj.no})}" style="width:500px;"></td>

🛠로직


Query Methods 사용하여 댓글 조회

Board.java

게시글 조회시 Board entity 를 조회할때
List<BoardReply> + List<BoardImage>를 같이 가져온다

	@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardReply> reply;

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) 
    @OrderBy(value = "no desc") // 정렬하기 no를 기준으로 내림차순
    List<BoardImage> image;

👨‍🏫대부분 양방향을 잘 쓰지 않는다
양방향 사용시 전체를 가져오는 경우,
용량커지고 속도가 느려지는 문제가 발생하기 때문에 잘 사용하지 않는다

List<BoardReply> 와 List<BoardImage> 데이터가 많은경우
➡️ 안그래도 양이 많아 속도가 느린데 이미지 용량은 더 커서 가져올때마다 용량과 속도측면에서 큰 문제점이 된다

양방향을 사용하지 않는경우 어떻게 댓글을 가져올수 있을까?
💡 Query Methods 사용하여 필요한 데이터만 조회하기!

BoardReplyRepository.java

BoardReplyRepository에서 Query Methods findBy~ 이용하여 댓글 조회하기

Board entity의 no를 가져올때 Board_no를 사용한다
Board_no = Board의 no
➡️ 엔티티 변수명에 언더바 사용시
엔티티 하위 위치 표기 인식이 안될수도 있기 때문에
엔티티 변수명에는 언더바를 사용한 변수는 사용하면 안된다!

@Repository
public interface BoardReplyRepository extends JpaRepository<BoardReply, Long>{
 
    // Board의 하위는 _ 언더바로 표시! 
    // Board_no = board의 no
    // 엔티티에는 언더바를 사용한 변수를 이용하면 안된다!
    // 엔티티 하위 위치 표기시 인식이 안될수도 있다
    List<BoardReply> findByBoard_noOrderByNoAsc(long no, Pageable pageable);
}

BoardController.java

brRepository.findByBoard_noOrderByNoAsc(no, pageRequest)를 사용하여 List<BoardReply> reply를 조회하기

    // 게시글 상세페이지로 이동
    @GetMapping(value = "/selectone.do")
    public ModelAndView selectOneGET(@RequestParam(name = "no") Long no) {
        Board board = bRepository.findById(no).orElse(null);

        // 댓글목록 확인용 출력 => stackoverflow
        System.out.println("=======board.getReply======");
        System.out.println(board.getReply().toString());

        // 이미지 확인용
        System.out.println("=======board.getImage======");
        System.out.println(board.getImage().toString());

        // 페이지네이션 가져오기
        PageRequest pageRequest = PageRequest.of(0, 5);
        System.out.println("===============pageRequest==================");
        System.out.println(pageRequest);

        List<BoardReply> reply = brRepository.findByBoard_noOrderByNoAsc(no, pageRequest);

        List<BoardImageProject> image = biRepository.findByBoard_noOrderByNoAsc(no);

        ModelAndView mav = new ModelAndView();
        mav.setViewName("board_selectone");
        // 게시글에 해당하는 댓글 목록 list도 board는 이미 갖고 있다
        mav.addObject("brd", board);
        mav.addObject("reply", reply);
        mav.addObject("image", image);

        return mav;
    }

댓글 삭제

board_selectone.html

댓글은 board no를 외래키로 참조하기 때문에 name="board.no"를 같이 넘겨줘야 한다

    <table border="1">
        <tr th:each="obj, idx : ${reply}">
            <td th:text="${obj.no}"></td>
            <td th:text="${obj.content}"></td>
            <td th:text="${obj.writer}"></td>
            <td th:text="${obj.regdate}"></td>
            <td>
                <form th:action="@{/reply/delete.do}" method="post">
                    <input type="text" th:value="${obj.no}" name="no" />
                    <input type="text" th:value="${brd.no}" name="board.no"/>
                    <input type="submit" value="삭제" />
                </form>
            </td>
        </tr>
    </table>

BoardController.java

	// 댓글 삭제하기
    @PostMapping(value="/delete.do")
    public String deletePOST(@ModelAttribute BoardReply breply){
        brRepository.deleteById(breply.getNo());
        return "redirect:/board/selectone.do?no="
            + breply.getBoard().getNo();
    }

게시글 일괄 삭제

체크박스 선택시 선택한 게시글 일괄삭제

board_select.html

  • script에서 일괄삭제를 처리한다 ➡️ type 값에 따라 삭제/수정 진행
  • 체크한 게시물의 번호를 전체 가져와서 배열로 넘긴다
  • 게시글 삭제시 _csrf토큰을 적용하여 Spring Security에서 post가 수행 가능하도록 한다
<body>
    <a th:href="@{/home.do}">홈으로</a>
    <a th:href="@{/board/insert.do}"><button>게시글 등록</button></a>

    <input type="button" value="게시글 일괄삭제" th:onclick="|javascript:handleBatch(1)|" />
    <a th:href="@{/board/insertbatch.do}"><button>게시글 일괄추가</button></a>
    <input type="button" value="게시글 일괄수정" th:onclick="|javascript:handleBatch(2)|" />

    <table border="1">
        <tr>
            <th>글번호</th>
            <th>글제목</th>
            <th>작성자</th>
            <th>글내용</th>
            <th>조회수</th>
            <th>등록일</th>
            <th>버튼</th>
        </tr>
        <tr th:each="obj, idx : ${list}">
            <td><input type="checkbox" th:value="${obj.no}" class="chk" /> </td>
            <td th:text="${obj.no}"></td>
            <td>
                <a th:href="@{/board/selectone.do(no=${obj.no})}" th:text="${obj.title}"></a>
            </td>
            <!-- <td th:text="${obj.title}"></td> -->
            <td th:text="${obj.writer}"></td>
            <td th:text="${obj.content}"></td>
            <td th:text="${obj.hit}"></td>
            <td th:text="${obj.regdate}"></td>
            <td>
                <form th:action="@{/board/delete.do}" method="post">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="삭제" />
                </form>
                <form th:action="@{/board/update.do}" method="get">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="수정" />
                </form>
                <!-- title누르면 댓글작성으로 이동 -->
                <form th:action="@{/boardimage/insert.do}" method="get">
                    <input type="hidden" name="no" th:value="${obj.no}" />
                    <input type="submit" value="이미지등록" />
                </form>
            </td>
        </tr>
    </table>
    </form>

    <script>
        const handleBatch = (type) => {
            // form태그 생성
            const form = document.createElement("form");
            if (type == 1) {
                form.action = "[[@{/board/deletebatch.do}]]";
                form.method = "post";
            } else if (type == 2) {
                form.action = "[[@{/board/updatebatch.do}]]";
                form.method = "get";
            }

            form.style.display = "none";

            document.body.appendChild(form);

            // 위의 체크박스 전체 가져오기
            const chk = document.getElementsByClassName("chk");
            for (let i = 0; i < chk.length; i++) {
                if (chk[i].checked) { //체크된 항목만 찾기
                    const check = document.createElement("input");
                    check.type = "checkbox";
                    check.name = "chk";
                    check.value = chk[i].value;
                    check.checked = true;

                    form.appendChild(check); // 체크된 항목만 form에 추가
                }
            }

            // <input type="hidden" name="_csrf" value="" />
            // 삭제시 _csrf 토큰적용
            // Spring Security를 이용한 CSRF Token 적용
            const csrf = document.createElement("input");
            csrf.type="hidden";
            csrf.name="_csrf";
            csrf.value="[[${_csrf.token}]]";
            form.appendChild(csrf);


            // form을 body에 추가
            document.body.appendChild(form);
            // form 전송
            form.submit();
        }
    </script>
</body>

BoardController.java

일괄삭제 하기

    @PostMapping(value = "/deletebatch.do")
    public String deleteBatch(
            @RequestParam(name = "chk") List<Long> chk) {
        bRepository.deleteAllById(chk);
        return "redirect:/board/select.do";
    }

0개의 댓글