Home 9. 값의 콜렉션 매핑
Post
Cancel

9. 값의 콜렉션 매핑

01. 값 콜렉션

JPA는 String, Int와 같은 단순 값에 대한 콜렉션을 지원한다.

또 @Embeddable로 설정한 밸류 값에 대한 콜렉션도 매핑할 수 있다. (List, Set, Map, Collection)

02. 단순 값 List 매핑

유적지 관광객을 위한 하루 이동 경로 목록을 제공하기 위한 모델을 예로 들면 관광 경로 정보를 담기 위한 클래스 Itinerary 클래스로 표현할 수 있다. 이 클래스에는 차례대로 이동할 관광지 목록을 저장하기 위해 List<String> 타입인 sites 속성을 정의하고 있다.

그리고 위 클래스를 DB에 매핑하려면 다음과 같이 두 개의 테이블을 사용한다.

Itinerary 클래스와 두 테이블 사이의 매핑은 @ElementCollection 애노테이션을 사용한다.

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
@Entity
public class Itinerary {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;

    @ElementCollection  //값 콜렉션임을 지정
    @CollectionTable(
        name="itinerary_site",   //콜렉션의 값은 itinerary_site 테이블에 저장
        joinColumns=@joinColumn(name="itinerary_id"))   //컬렉션이 속할 엔티티 식별자 컬럼 지정
    @OrderColumn(name="list_idx")       //리스트의 인덱스 값 컬럼 지정
    @Column(name="site")
    private List<String> sites;

    //...

    public void changeSites(List<String> sites) {
        this.sites = sites;
    }

    public void clearSites() {
        sites.clear();
    }
}

2.1 List의 저장과 조회

1
2
3
4
5
em.getTransaction().begin();
List<String> sites = Arrays.asList("경복궁", "청계천", "명동", "인사동");
Itinerary itinerary  = new Itinerary("광화문-종로 인근", "설명", sites);
em.persist(itinerary);
em.getTransaction().commit();

위 코드를 실행한 쿼리 결과를 보면 sites 속성에 저장된 각 값의 인덱스 값을 list_idx 컬럼에 저장하는 것을 볼 수 있다.

@ElementCollection 애노테이션의 fetch 속성은 기본값이 LAZY라서 sites.get(0) 코드를 실행하는 시점에 데이터를 읽어온다.

2.2 List 변경

Itinerary 클래스는 다음의 두 가지 방법으로 sites 콜렉션의 값을 변경할 수 있다.

  • changeSites() 메서드를 이용해서 sites 속성에 새로운 컬렉션을 할당 > 실행한 쿼리를 보면 delete 쿼리를 이용해 기존 컬렉션 데이터를 삭제한 후 insert 쿼리를 이용해서 새로운 컬렉션의 데이터를 추가한다.
  • getSites() 메서드로 구한 컬렉션을 수정 > 기존 항목을 변경하면 update, 새로 추가하면 insert 쿼리를 실행한다.

2.3 List 전체 삭제

1
2
3
4
em.getTransaction().begin();
Itinerary itinerary = em.find(Itinerary.class, 1L);
itinerary.cleatSites();  //sites.clear()
em.getTransaction().commit();

컬렉션의 데이터를 삭제하려면 clear() 메서드를 사용하거나 null을 할당하는 방법이 있다.

03. 밸류 객체 List 매핑

Itinerary 클래스의 이동경로 저장시 단순 장소 이름뿐만 아니라 관광 시간도 포함하려할때 String 대신 다음의 밸류 타입을 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SiteInfo {
    private String site;
    private int time;
    //...
}

public class itinerary {
    //...

    @ElementCollection
    @CollectionTable(
        name="itinerary_site",
        joinColumns=@joinColumn(name="itinerary_id"))
    @OrderColumn(name="list_idx")
    //@Column이 없어진거 말고는 설정이 똑같다
    private List<SiteInfo> sites;

}

동작 방식은 단순 값 리스트의 매핑과 동일하다.

그리고 @ElementCollection로 매핑한 클래스의 컬럼 이름 대신 다른 컬럼 이름을 사용하고 싶다면 @AttrubuteOverride 애노테이션이나 @AttributeOverrides 애노테이션을 사용하면 된다.

04 List 요소와 null

1
2
3
4
em.getTransaction().begin();
Itinerary itinerary = em.find(Itinerary.class, 1L);
itinerary.getSites().set(1, null);
em.getTransaction().commit();

List의 전체 길이가 4라고 할 때 중간에 인덱스 1을 삭제하면 테이블에 인텍스 1에 해당하는 데이터가 null로 할당된다.

1
2
3
4
Itinerary itinerary = em.find(Itinerary.class, 1L);
List<String> sites = itinerary.getSites();
sites.size() == 4;      //true
sites.get(1) == null;   //true

05. 단순 값 Set 매핑

집합은 중복을 허용하지 않는 컬렉션이다. 예) 사용자마다 관심사를 위한 키워드를 등록하는 경우

1
2
3
4
5
6
7
8
9
10
public class User {
    private String email;

    //...

    private Set<String> keywords = new HashSet<>();

    //getter, setter
}

1
2
3
4
5
Set<String> keywords = new HashSet<>();
keywords.add("역사");
keywords.add("유적");
keywords.add("전통음식");
user.setKeywords(keywords)
user_emailkeyword
user@email.com역사
user@email.com유적
user@email.com전통음식
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name="user")
public class User {

    @Id@Basic
    private String email;
    @Basic private String name;

    @Basic @Temporal(TemporalType.TIMESTAMP)
    @Column(name="create_date")
    private Date createDate;

    @ElemtentCollection
    @CollectionTable(
        name="user_keyword",
        joinColumns=@JoinColumn(name="user_email"))
    @Column(name="keyword")
    private Set<String> keywords = new HashSet<>();

    //…
}

@OrderColumn 애노테이션을 사용하지 않는 것을 제외하면 List 타입의 단순 값을 매핑할 때와 같다

5.1 Set의 저장과 조회

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
em.getTransaction().begin();
User user = new User("user@email.com","사용자", new Date());
Set<String> keywords = new HashSet<>();
keywords.add("역사");
keywords.add("유적");
keywords.add("전통음식");
//Set을 생성하여 보관된 값을 user의 keywords로 전달한다.
user.setKeywords(keywords);
//User 객체만 저장하면 @CollectionTable로 지정한 테이블에 Set에 보관된 값을 함께 저장한다.
em.persist(user);
em.getTransaction().commit();



User user = em.find(User.class, email);
Set<String> keywords = user.getKeywords();

//@ElementCollection 애노테이션은 기본 값이 LAZY라 실제 데이터에 접근시 user_keyword 테이블을 조회한다.
for(String keyword: keywords) {
    //keyword 사용
}

5.2 Set의 변경

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
//기존 값 중 "서울"을 삭제하고, 새로운 값인 "한성"을 추가한다.
em.getTransaction().begin();
User user = em.find(User.class, email);
Set<String> keywords = user.getKeywords();
keywords.remove("서울");
keywords.add("한성");
em.getTransaction().commit();

//새로운 Set 객체를 할당하면 delete 수행 후 insert 한다.
em.getTransaction().begin();
User user = em.find(User.class, email);
Set<String> keywords = new HashSet<>();
keywords.add("부여");
keywords.add("한성");
user.setKeywords(keywords); //새로운 set 할당
em.getTransaction().commit();

//Set clear()를 사용하는 경우 전체 Set을 삭제하기 위한 delete 쿼리를 수행하지 않는다.
//기존 Set 값과 비교하여 삭제된 요소만 delete 쿼리로 삭제하고 새로 추가된 요소만 insert 쿼리로 추가한다.
em.getTransaction().begin();
User user = em.find(User.class, email);
Set<String> keywords = user.getKeywords();
keywords.clear();  //기존 Set의 데이터를 삭제
keywords.add("부여");
keywords.add("한성");
em.getTransaction().commit();

5.3 Set 전체 삭제

Set의 데이터를 삭제하고 싶다면 clear() 메서드를 실행하거나 빈 Set을 할당하거나 null을 할당하면 된다.

06. 밸류 객체 Set 매핑

예) 관광지 정보에 이름과 타입을 함께 표현하려는 경우 기존처럼 String 타입이 아닌 다음과 같이 해야한다.

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
@Embeddable
public class RecItem {
    private String name;
    private String type;

    //...

    @Override
    public boolean equals(Object o) {
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        RecItem recItem = (RecItem)0;
        return Objects.equals(name, recItem.name) && Objects.eqauls(type, recItem.type);
    }

    @Override
    public int hashCode(){
        return Objects.hash(name, type);
    }
}

public class Sight {
    //…
    private Set<RecItem> recItems;
}

6.1 Set에 저장할 밸류 클래스의 equals() 메서드와 hashCode() 메서드

RecItem 클래스에서 눈여겨 봐야할 점은 equals() 메서드와 hashCode() 메서드이다. 이 메서드들을 재정의한 이유는 Set의 특성 때문이다. Set은 중복을 허용하지 않기 때문에 두 값이 같은지 여부를 비교하기 위해 equals() 메서드를 사용한다. hashCode()를 재정의한 이유는 하이버네이트가 Set 타입에 대해 HashSet을 사용하기 때문이다. HashSet은 해시 코드를 사용해서 데이터를 분류해서 저장하는데, 이 해시코드를 구할 때 hashCode() 메서드를 이용한다. 같은 값을 갖는 객체는 같은 해시코드를 리턴해야 올바르게 동작하므로 Set에 보관할 객체는 equals(), hashCode()를 알맞게 구현해야 한다.

07. 단순 값 Map 매핑

Map은 (키, 값) 쌍을 저장하기 위한 컬렉션 타입이다. 엔티티의 정해진 속성이 아니라 자유롭게 엔티티의 값을 설정하고 싶을 때 유용하게 사용할 수 있다.

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
public class Hotel {
    @Id
    private String id;

    //...

    @ElementCollection
    @CollectionTable (
        name="hotel_property",
        joinColumns = @JoinColumn(name="hotel_id")
    )
    @MapKeyColumn(name="prop_name") //컬렉션 테이블에서 Map의 키로 사용할 컬럼을 지정한다.
    @Column(name="prop_value")
    private Map<String, String> properties;

    //...

    public void addProperties(String name, String value){
        this.properties.put(name, value);
    }

    public Map<String, String> getProperties() {
        return properties;
    }
}

7.1 Map의 저장과 조회

1
2
3
4
5
6
7
8
9
10
11
em.getTransaction().begin();
Hotel hotel = new Hotel("H-GURO", "구로 호텔", Grade.STAR4, new Address("12345", "addr1", "addr2"));
hotel.addProperty("추가1", "추가정보1");
hotel.addProperty("추가2", "추가정보2");
em.persist(hotel);
em.getTransaction().commit();


Hotel hotel = em.find(Hotel.class, "H100-01");
Map<String, String> properties = hotel.getProperties();
String viewValue = properties.get("VIEW");      //지연로딩이 기본값이므로 실제 엔티티에 접근할 때 select 쿼리 실행

7.2 Map의 변경

  • put(k, v) : 키에 대해 값을 추가하거나 기존 값을 변경
  • remove(k) : 키에 대한 값을 삭제

7.3 Map의 전체 삭제

Map 데이터 삭제는 clear() 메서드로 삭제하거나 데이터가 없는 빈 Map을 할당하거나 null을 할당하면 된다.

08. 밸류 객체 Map 매핑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Embeddable
public class PropValue{
    @Column(name="prop_value")
    private String value;
    private String type;

    //생성자, get, equals..

}

@Entity
public class Hotel {
  //...
  @ElementCollection
  @CollectionTable(
    neme="hotel_property",
    joinColumn=@JoinColumn(name="hotel_id")
  )
  @MapKeyColumn(name="prop_name")
  private Map<String, String> properties = new HashMap<>();
}

09. 콜렉션 타입별 구현 클래스

엔티티를 로딩할 때 하이버네이트는 다음 클래스를 이용해서 각 콜렉션 타입의 인스턴스를 생성한다.

  • List -> ArrayList
  • Set -> HashSet
  • Map -> HashMap

타입을 다르게 초기화 하더라도 하이버네이트는 엔티티 로딩시 위의 타입으로 객체를 보관한다.

10. 조회할 때 정렬 Set과 정렬 Map 사용하기

하이버네이트는 컬렉션 데이터를 조회해서 생성하는 시점에 정렬해서 읽어오는 방법을 제공하고 있다.

  1. 메모리상에서 정렬
  2. SQL 실행시 order by를 사용
1
2
3
4
5
6
7
8
9
10
11
//Set의 경우 SoredSet과 자바의 Comparator를 사용해서 데이터 정럴할 수 있다.
@Entity
public class User {
  @ElementCollection
  @CollectionTable(
    name="user_keyword",
    joinColumns=@JoinColumn(name="user_email"))
  @Column(name="keyword")
  @org.hibernate.annotation.SortNatural
  private SortedSet<String> keywords = new TreeSet<>();
}

SortNatural를 사용하면 Set에 보관된 객체가 Comparable 인터페이스를 구현했다고 가정하고 compareTo() 메서드를 이용하여 정렬한다.

컬렉션에 사용한 타입이 Comparable를 구현하지 않았다면 @SortComparator 사용해서 정렬할 때 사용할 Comparator 클래스를 지정할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//SQL의 order by를 사용한 정렬
@ElementCollection
@CollectionTable(
  name="sight_rec_item",
  joinColumns=@JoinColumn(name="sight_id"))
//이 방법은 SQL 쿼리를 입력하는 것이다. 즉 테이블의 namd 컬럼을 정렬 대상으로 하는 것
@org.hibernate.annotation.OrderBy(clause="name asc")
private Set<RecItem> recItem = new LinkedHashSet<>();

//JPA가 제공하는 @OrderBy를 사용한 정렬
@ElementCollection
@CollectionTable(
  name="sight_rec_item",
  joinColumns=@JoinColumn(name="sight_id"))
//이 방법은 JPA의 정렬 대상 객체의 속성을 사용한다, 즉 RecItem 객체의 name속성을 정렬한다고 설정
@javax.persistence.OrderBy("name asc")
private Set<RecItem> recItem = new LinkedHashSet<>();

Map의 경우도 Set과 동일하게 사용하여 정렬할 수 있다.

This post is licensed under CC BY 4.0 by the author.