티스토리 뷰
안녕하세요 강정호입니다. 오늘을 스프링 부트에서 엔티티를 맵핑하는 방법과 테스트 코드 작성에 대해 알아볼게요.
이렇게 Category와 Board는 다음과 같이 1:N 관계를 맺고 있습니다.
1개의 카테고리는 여러개의 게시물을 갖는 것이지요.
그렇다면 엔티티는 다음과 같이 생성됩니다.
Board 엔티티
@Entity
@Table(name = "board")
@Getter
@Setter
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String title;
private String content;
private int readCount;
private LocalDateTime createDate;
@ManyToOne
@JoinColumn(name = "category_id") //실제 FK의 컬럼명이 된다.
private Category category; // 이 보드가 어떤 카테고리에 속해 있는지를 식별해주는 FK
1) @ManyToOne : Board 측면에서 Category와의 관계는 Many - One 관계이기 때문에 @ManyToOne 어노테이션을 사용하게 됩니다.
2) @JoinColumn : FK를 가지는 엔티티가 @JoinColumn 어노테이션을 사용합니다. 논리적으로 Board가 어떤 카테고리에 속하는지를 식별해야하기 때문에 Board가 FK를 가집니다.
3)name="category_id" : 이것이 실제 Board 테이블에 있는 Category테이블의 FK 컬럼명이 된다.
Category 엔티티
@Entity //엔티티이기 때문에 어노테이션 붙인다
@Table(name="category") //엔티티와 관련을 맺고 있는 테이블이 category 테이블이라고 명시
@Getter
@Setter
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy="category")
private List<Board> boards;
1) @OneToMany : Category의 입장에서 Board를 여러개 가지기 때문에 One - Many 관계이다.
2) mappedBy : Category가 Board와 어떤 관계를 가지고 있는지를 표시하는 속성이다. Board는 @JoinColumn을 사용해서 Category와의 관계를 표시하지만, Category는 mappedBy를 이용해서 Board와의 관계를 표시한다. mappedBy에 들어가는 것은 Board에서의 Category 객체 변수이다.
** FK를 가지면? @JoinColumn 사용. // FK를 안가지면? mappedBy 사용.
BoardRepository 생성
package examples.boot.myshop.repository;
import examples.boot.myshop.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<Board, Long> {
/*
* 이렇게만 인터페이스를 선언해도
* 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
* 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
* Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
* */
CategoryRepository 생성
import examples.boot.myshop.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryRepository extends JpaRepository<Category, Long> {
/*
* 이렇게만 인터페이스를 선언해도
* 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
* 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
* Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
* */
}
테스트 코드1 : 같은 트랜잭션 내에서의 1차 캐시
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional // test에서 @Transactional을 사용하면 자동 롤백된다.
public class MyshopApplicationTests {
@Autowired //테스트할 클래스를 오토와이어로 주입
CategoryRepository categoryRespository;
@Autowired
BoardRepository boardRepository;
@Test
public void contextLoads() {
}
@Test
public void test1(){
Category category=categoryRespository.getOne(1L);
System.out.println(category.getId());
System.out.println(category.getName());
Category category2 = categoryRespository.getOne(1L);
if(category==category2){
System.out.println("category==category2");
}
위의 test1()의 결과는 다음과 같다.
이상하지 않나요?? 분명히 getOne() 이라는 메서드를 2번 썼는데, 실제로 쿼리는 1번만 작성되었어요.
그 이유는 같은 트랜잭션 내부에서는 1차 캐시가 적용되기 때문입니다. category 인스턴스가 persistence context의 1차 캐시에 저장되어 있어서 2번째 getOne()을 할 때는 캐시에 있던 인스턴스가 반환된 것입니다.
엔티티 매니저가 persist() 또는 find()를 하면 그 엔티티는 managed 상태가 되면서 persistence context의 1차 캐시에 저장이 됩니다. 1차 캐시는 일종의 map으로 생각하면 되는데 key는 @Id이고, value는 엔티티 인스턴스가 된다.
테스트 코드2 : 프록시 객체와 쿼리 실행 시점
@Test
public void test2(){
System.out.println("------------------------------");
System.out.println(categoryRespository.getClass().getName()); //프록시 객체 출력
Category category = categoryRespository.getOne(1L); //Category 쿼리문 실행
List<Board> boards=category.getBoards(); //Board 쿼리가 실행되지 않았다.
System.out.println("------------------------------");
System.out.println(boards.getClass().getName()); //PersistenceBag
System.out.println("------------------------------");
for(Board board : boards){ //실제로 Board 쿼리가 실행된 시점
System.out.println(board.getTitle());
}
System.out.println("------------------------------");
위의 테스트 코드 결과는 다음과 같다
쿼리가 실행되지 않고, getTitle 할 때 쿼리가 실행된 이유?? 이건 선생님께 다시 질문
테스트 코드 3 : 쿼리 메서드를 이용해서 값 가져오기
@Test
public void test3(){
List<Board> list=boardRepository.findAllByName("kim");
//System.out.println(list);
for(Board board : list){
System.out.println(board.getTitle());
}
}
실제로 findAllByName 이라는 메서드는 없다. 하지만 BoardRepository에서 해당 메서드를 만들 때 메서드의 이름이 자동완성 되는 것을 알 수 있다. 그 이유는 Spring Data JPA가 메서드 이름으로 쿼리문을 만들기 때문이다. 그것이 바로 "Query Method"이다.
public interface BoardRepository extends JpaRepository<Board, Long> {
public List<Board> findAllByName(String name);
}
결과는 다음과 같다.
테스트 코드 4 : 1+N문제 코드와 JPQL
@Test
public void test4(){
List<Board> list1=boardRepository.findAll(); //이것은 1+N문제를 발생시킨다.
List<Board> list2=boardRepository.getBoards();//쿼리가 1번만 실행된다. 조인한 결과를 가져올 수 있다.
for(Board board : list2){
System.out.println(board.getTitle());
System.out.println(board.getCategory().getName());
}
}
쿼리 결과문
노란색 사각형이 1+N 쿼리를 실행하는 부분이고
연두색 사각형이 JPQL을 사용하여 쿼리를 1번만 실행시키는 부분이다.
밑에 출력문은 JPQL을 이용하여 출력한 결과이다.
그럼 1+N 문제는 무엇일까? 1+N 문제 <--- 여기 링크에서 확인하라
'Back-end' 카테고리의 다른 글
패스트캠퍼스 스프링부터 4번쨰 수업 (0) | 2018.10.27 |
---|---|
영속성 컨텍스트(Persistence Context) (0) | 2018.10.26 |
[Spring Boot] JPA (0) | 2018.10.20 |
[Spring Boot] JPA(강사님 수업 내용) (0) | 2018.10.20 |
[Spring Boot] 트랜잭션 (0) | 2018.10.20 |
- Total
- Today
- Yesterday
- Spring boot
- 항해솔직후기
- 도커
- 월급쟁이부자들
- 2023년
- push_back
- 깃
- Use case
- ```````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
- front
- 재테크공부
- GIT
- 유즈케이스
- 항해플러스백엔드
- pop_back
- 부동산공부
- 내년은 빡세게!!
- 월부닷컴
- resize
- 열반스쿨기초반
- Inception
- 깃허브
- 인셉션
- github
- 항해플러스후기
- 개발자 회고
- 관계대수
- docker
- 폭포수
- 파라메터
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |