스프링부트에서 JPA-MySQL 연동하기 3: 1:N 연관관계 설정

2020. 3. 26. 17:42SpringBoot

지난 번까지는 각 테이블 간의 연관관계를 하나도 지정해주지 않고 기본적인 작동 방식만 알아보았다. 이번에는 PK-FK로 연관된 테이블의 관계를 엔티티에서 역시 맺어보고자 한다.

이 프로젝트의 테이블들의 상관관계는 다음과 같다.

상품과 관련된 prod 테이블이 있고, 상품을 조회하면 글/이미지 쌍으로 디테일이 있는 prod_detail 테이블이 있다. 두 테이블은 1(prod) : N(prod_detail) 관계이다. prod 테이블의 no 컬럼이 PK이며, prod_detail의 prod_no 컬럼이 prod 테이블을 참조하는 컬럼이다. 상품이 삭제되면 디테일도 삭제되어야 하기 때문에 양방향 관계를 가진다.

이 방향이라는 것은, 양방향/ 단방향이 있다.

테이블 개념에서 볼 때 PK-FK로 연관관계를 맺고 있으면 방향이랄 것이 없다.

하지만 JPA 환경에서 엔티티들은 기본적으로 단방향이다. Prod 엔티티에 @OneToMany 어노테이션으로 ProdDetail을 명시해주면 Prod->ProdDetail의 방향이 된다. 테이블의 양방향 상태처럼 만들어주려면 ProdDetail 엔티티에서도 @ManyToOne 설정을 해 주어야 한다.

 

@Getter
@Setter
@ToString
@Table(name = "prod")
@Entity
public class Prod {

	@Id
	@GeneratedValue
	@Column(name = "no")
	private Long no;
	private String name;
	private String thumbnailUrl;
	private Long originPrice;
	private Long discPrice;
	private String description;
	private LocalDateTime createdAt;
	@Transient
	private boolean inBasket;

	@PrePersist
	public void createdAt() {
		this.createdAt = LocalDateTime.now();
	}

	@OneToMany(mappedBy = "prod", fetch = FetchType.EAGER)
	private List<ProdDetail> detailList = new ArrayList<ProdDetail>();
}
@Getter
@Setter
@ToString
@Table(name = "prod_detail")
@Entity
public class ProdDetail {

	@Id
	private Long id;
//	@Column(name = "prod_no")
//	private Long prodNo;
	private String content;
	private String imageUrl;
	private LocalDateTime createdAt;

	@PrePersist
	public void createdAt() {
		this.createdAt = LocalDateTime.now();
	}

	@ManyToOne
	@JoinColumn(name = "prod_no", nullable = false, updatable = false)
	@JsonIgnore
	private Prod prod;

	public void setProd(Prod prod) {
		if (this.prod != null) {
			this.prod.getDetailList().add(this);
		}
		this.prod = prod;
		prod.getDetailList().add(this);
	}

}

@OneToMany의 mappedBy 속성은 양방향 매핑할 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 준다. 이렇게 되면 서로 참조할 수 있는 구조를 가진다.

1:N에서 N입장인 ProdDetail에서 @JoinColumn을 명시해주어야 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다. 반드시 name 속성에 참조하는 테이블(prod)_기본키(no) 형태로 적어주어야 한다.

이렇게 하면 양방향 관계 설정이 된다.

 

상품을 조회할 때 상품과 디테일의 글/이미지 모두를 조회하는 API를 만들어보고자 한다.

@RestController
@RequestMapping("/prod")
public class ProdController {

	private static final Logger logger = LoggerFactory.getLogger(ProdController.class);

	@Autowired
	ProdService prodService;

	@GetMapping("/{id}")
	public ResponseVO findProdWithDetailByUser(@CookieValue(value = "accesstoken", required = false) String accesstoken,
			@PathVariable("id") Long no) throws Exception {
		logger.info("call findProdWithDetailByUser()");
		
		Optional<Prod> prodResult = prodService.findProdWithDetailByUser(accesstoken, no);
		ResponseVO result = new ResponseVO();
		result.setCode(HttpStatus.OK);
		result.setMessage("SUCCESS");
		result.setData(prodResult);
		return result;
	}	
}
@Service
public class ProdService {

	private static final Logger logger = LoggerFactory.getLogger(ProdService.class);

	@Autowired
	TokenRepository tokenRepo;

	@Autowired
	ProdRepository prodRepo;

	@Autowired
	BasketRepository basketRepo;

	public Optional<Prod> findProdWithDetailByUser(String accesstoken, Long no) {
		logger.info("call findProdWithDetailByUser()");

		Optional<Prod> prodResult = prodRepo.findById(no);

		Token token = tokenRepo.findByToken(accesstoken);
		String email = token.getUserEmail();
		Basket basket = basketRepo.findByUserEmailAndProdNo(email, no);
		if (basket != null) {
			boolean inBasket = true;
			prodResult.get().setInBasket(inBasket);
			return prodResult;
		} else {
			return prodResult;
		}
	}

}

(로그인했을 시 장바구니에 담겨 있는지 없는지 여부를 확인하는 로직도 있다.)

findById 메소드를 사용할 것이기 때문에 레포지토리에 따로 메소드를 생성할 필요는 없다.

 

포스트맨을 사용해서 1번 상품을 조회한다.

성공적으로 상품을 조회한다.

콘솔을 확인하면, findById 메소드가 실행되었을 때 left outer join으로 prod_detail의 데이터까지 조회한 것을 확인할 수 있다.

 

@OneToMany, @ManyToOne 등의 설정은 JPA로 테이블 간의 관계 설정을 보다 손쉽게 해 주는  또 하나의 특징적인 기능이라는 것을 이번 프로젝트 때 알게 되었다.

참조한 블로그