Spring Data JPA는 Spring에서 제공하는 모듈 중 하나로, 개발자가 JPA를 더 쉽고 편하게 사용할 수 있도록 도와준다. JPA를 한 단계 추상화시킨 Repository
라는 인터페이스를 제공한다
Query Methods = 쿼리문과 같은 역할
springdocs Query Methods 링크
테이블 생성
@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
@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;
}
JpaRepository 상속받아 사용
➡️ extendsJpaRepository<엔티티명, 해당 엔티티의 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);
}
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
를 변수로 명시하여 사용
# 페이지네이션 가져올 글 개수의 변수
item.list.size=10
# 기본이미지 변수
default.image=classpath:/static/image/noimage.jpg
global.properties 생성 후 application에 파일 등록을 해야 사용할 수 있다
// 환경변수 파일 사용 설정
@PropertySource("global.properties")
➡️ Application등록 후 사용을 원하는 Controller에 가서 명시해준다
@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;
@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)
검색어에 해당하는 글 개수 세기
// SELECT COUNT(*) i FROM Item i Where i.name LIKE '%' || ?1 || '%'
long countByNameContaining(String name);
전체개수에서 검색결과의 갯수 구하기
long count = iRepository.countByNameContaining(name);
...
mav.addObject("count", count);
페이지네이션 버튼 생성을 위한
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;
}
<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
옵션값은 parent-child 관계 일 때@OneToMany
,@ManyToOne
의 옵션 값이며, Entity의 상태가 변화했을 때 관계가 있는 Entity에도 상태 변화를 전파시키는 옵션이다
여러개의 Cascade
타입 중 remove
를 이용하면
연관 관계에 있는 영속 인스턴스를 연쇄적으로 삭제한다
CascadeType.REMOVE
는 관계가 끊어졌다는 것을 제거로 보지 않기 때문에,
관계가 끊어진 child를 자동 제거하지는 않는다
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
➡️ cascade 옵션을 추가하면 원본이 지워질때 외래키로 가진 정보들이 모두 지워진다
= 원본데이터를 지우면 해당 외래키의 레퍼런스(=참조값)도 지워진다
...
@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
관계를 갖고있다
BoardImage/BoardReply
는
Board entity의 no를 참조할 때 Board entity를 전부 가져온다
➡️ entity 전체를 가져오는게 아닌 entity에서 필요한 데이터만 가져와서 사용해보기
referencedColumnName = "NO"
를 추가하여,
board entity 전체가 아닌 board의 no만 가져오게 설정
@JoinColumn(name = "BRDNO", referencedColumnName = "NO")
Board board;
@Repository
public interface BoardReplyRepository extends JpaRepository<BoardReply, Long>{
// Board의 하위는 _ 언더바로 표시!
// Board_no = board의 no
// 엔티티에는 언더바를 사용한 변수를 이용하면 안된다!
// 엔티티 하위 위치 표기시 인식이 안될수도 있다
List<BoardReply> findByBoard_noOrderByNoAsc(long no, Pageable pageable);
}
BoardImage entity중 일부 컬럼만 가져오기
BoardImage entity의 항목 중 이미지 번호, 게시글 번호만 가져오기
// enttity의 항목중에서 일부 컬럼만 가져오기
public interface BoardImageProject {
// Long no;
Long getNo();
// Board board;
Board getBoard();
}
이미지는 이미지 번호만 알 수 있다면,
이미지 url 생성이 가능하기 때문에 모든 이미지 정보가 필요하지 않다
@Repository
public interface BoardImageRepository extends JpaRepository<BoardImage, Long>{
List<BoardImageProject> findByBoard_noOrderByNoAsc(long no);
}
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 엔티티에 포함되어있는 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>
게시글 조회시 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에서 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);
}
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 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>
// 댓글 삭제하기
@PostMapping(value="/delete.do")
public String deletePOST(@ModelAttribute BoardReply breply){
brRepository.deleteById(breply.getNo());
return "redirect:/board/selectone.do?no="
+ breply.getBoard().getNo();
}
체크박스 선택시 선택한 게시글 일괄삭제
- 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>
일괄삭제 하기
@PostMapping(value = "/deletebatch.do")
public String deleteBatch(
@RequestParam(name = "chk") List<Long> chk) {
bRepository.deleteAllById(chk);
return "redirect:/board/select.do";
}