Home 8. 엔티티간 N:1 단방향 연관
Post
Cancel

8. 엔티티간 N:1 단방향 연관

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");
    }
}
This post is licensed under CC BY 4.0 by the author.