01. 엔티티의 N:1 연관
엔티티를 구현하다보면 같은 타입의 여러 엔티티 객체가 다른 타입의 한 엔티티를 참조해야 할 때가 있다.
예) 호텔에 대한 리뷰처럼 한 개의 호텔에 대해 다수의 리뷰를 달 수 있는 경우
이때 리뷰 입장에서 보면 한개 이상의 리뷰가 한 개의 호텔을 참조하게 된다. 즉 리뷰와 호텔은 N:1 관계를 갖는다.
02. 참조키를 이용한 N:1 연관 설정
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
@Entity
@Table(name="hotel_review")
public class Review {
@Id
@GeneratedValue(strategy=GenerationType.IENTITY)
private Long id;
//Review 입장에서 다수의 Review 엔티티가 한 개의 Hotel 엔티티를 참조하는 N:1 연관임을 설정
@ManyToOne
@JoinColumn(name="hotel_id")
private Hotel hotel;
private int rate;
private String comment;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="rating_date")
private Date ratingDate;
protected Review() {}
public Review(Hotel hotel, int rate, String Comment, Date ratingDate) {
this.hotel = hotel;
this.rate = rate;
this.comment = commnet;
this.ratingDate = ratingDate;
}
}
03. N:1의 연관 엔티티 로딩
@ManyToOne 애노테이션의 fetch 속성도 기본 값이 EAGER를 사용한다. 여기에도 지연 로딩을 사용하고 싶다면 fetch 속성의 값으로 FetchType.LAZY를 사용하면 된다.
1
2
3
4
5
Review review1 = em.find(Review.class, 1L);
Review review2 = em.find(Review.class, 2L);
Hotel hotel1 = review1.getHotel();
Hotel hotel2 = review2.getHotel();
//hotel1 == hotel2 true
review1과 review2가 참조하는 Hotel의 식별자가 같다면 hotel1, hotel2는 동일 객체다 영속 컨텍스트에서는 식별자를 기준으로 엔티티를 저장하고, 추적하기 때문에 동일한 Hotel 데이터를 두 번 읽어와도 영속 컨텍스트에는 먼저 로딩한 Hotel 엔티티만 존재한다.
04. 특정 엔티티와 N:1 연관을 맺은 엔티티 목록 구하기
N:1 연관을 갖는 엔티티에 대해 가장 많이 사용하는 기능 중 하나는 특정 엔티티와 N:1 연관을 맺은 엔티티 목록 구하기이다. 예) 호텔의 리뷰를 보기 위해 특정 Hotel 엔티티와 관련된 Review 목록을 구하는 기능
JPA는 JPQL이라는 쿼리 언어를 이용해서 특정 엔티티 목록을 조회하는 방법을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
Hotel hotel = em.find(Hotel.class, "H100-01");
TypedQuery<Review> query = em.createQuery(
"select f from Review r where r.Hotel = :hotel" +
"order by r.id desc". Review.class);
query.setParameter("hotel", hotel);
//모든 엔티티를 조회하지 않고 일부만 조회하기 위한 세팅
query.setFirstResult(3);
query.setMaxResults(3);
List<Review> reviews = query.getResultList();
05. 호텔과 최신 리뷰 조회하는 기능 만들기
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public class HotelRepository{
EntityManager em = EMF.currentEntityManager();
return em.find(Hotel.class, id);
}
public class ReviewRepository {
public List<Review> findByHotel(Hotel hotel, int startRow, int maxResults) {
TypedQuery<Review> query = EMF.CurrentEntityManager()
.createQuery("select f from Review r where r.Hotel = :hotel" +
"order by r.id desc". Review.class);
query.setParameter("hotel", hotel);
query.setFirstResult(startRow);
query.setMaxResults(maxResults);
return query.getResultList();
}
}
public class HotleSummary {
private Hotel hotel;
private List<Review> latestReviews;
public HotleSummary(Hotel hotel, List<Review> latestReviews){
this.hotel = hotel;
this.latestReviews = latestReviews;
}
//getter..
}
public class GetHotelSummaryServiece {
private HotelRepository hotelRepository = new HotelRepository();
private ReviewRepository hotelRepository = new ReviewRepository();
public HotelSummary getHotelSummary(String hotelId) {
try{
Hotel hotel = hotelRepository.find(hotelId);
List<Review> latestReviews = reviewRepository.findByHotel(hotel, 0, 3);
return new HotelSummary(hotel, latestReviews);
}finally {
EMF.closeCurrentEntityManager();
}
}
}
public class HotelNotFoundException extends RuntimeException{}
public class HotelMain {
private static GetHotelSummaryServiece hotelSummaryServiece = new GetHotelSummaryServiece();
public static void main(String[] args) throw IOException {
EMF.init();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try{
while(true) {
System.out.println("명령어를 입력하세요.");
String line = readerLine();
String[] commands = line.split(" ");
if(commands[0].equalsIgnoreCase("exit")) {
System.out.println("종료합니다.");
break;
}else if(commands[0].equalsIgnoreCase("view")){
handleViewCommand(commands);
}else{
System.out.println("올바른 명령어를 입력하세요");
}
System.out.println("----");
}
}finally {
EMF.close();
}
}
private static void handleViewCommand(String[] commands) {
if(commands.length == 1) {
printJHelp();
}else {
String hotelId = commands[1];
try{
HotelSummary hotelSummary = hotelSummaryService.getHotelSummary(hotelId);
Hotel hotel = hotelSummary.getHotel();
System.out.printf("ID: %s\n 이름: %s\n 등급: %s\n", hotel.getId(), hotel.getName(), hotel.getGrade().name());
List<Review> reviews = hotelSummary.getLatestReviews();
if(reviews.isEmpty()) {
System.out.println("*리뷰 없음");
}else {
reviews.forEach(review -> System.out.printlf("리뷰 점수: %d, 내용: %s\n", review.getRate(), review.getCommand()));
}
}catch(HotelNotFoundException e) {
System.out.printf("호텔[%s] 정보가 없습니다.\n", hotelId);
}
}
}
private static void printHelp() {
System.out.println("사용법: view 호텔ID");
}
}