본문 바로가기

Homo Faber/Idioms

Hibernate3 사용시 Join 테이블에 FK 이외의 컬럼이 있는 경우 매핑

ORM을 사용하다 보면 항상 겪는 문제지만, 다대다(many-to-many) 관계를 객체와 어떻게 매핑할 것인가이다. 물론, 객체 입장에서는 두 객체간의 Collection 문제지만, DB 입장에서는 항상 Join 테이블이 끼어 있어서 이를 해결하기간 웬만해서는 쉽지 않다.

Hibernate3는 EJB3의 JPA(Java Persistence API)의 구현체로 이에 대한 문제를 다음과 같이 해결하고 있다.

사용자 삽입 이미지


위의 그림과 같이 Category에 Item을 추가할 때마다 어떤 정보가 필요하다고 가정하자.
위의 구조를 자바 클래스에 매핑하는 데에 두가지 방법이 있다. 첫번째 방법은 join 테이블에 대한 중간의 entity 클래스를 사용하여 일대다(one-to-many) 관계로 매핑하는 것이다. 두번째 방법은 join 테이블에 대한 value-type을 사용해서 component의 집합을 이용하는 것이다.

중간의 entity를 join 테이블에 매핑
첫번째 방법은 중간의 entity 클래스인 CategorizedItem을 사용해서 Category와 Item 사이의 다대다 관계를 해결하는 것이다.
@Entity
@Table(name="CATEGORIZED_ITEM")
public class CategorizedItem {
   @Embeddable
   public static class Id implements Serializable {
      @Column(name="category_id") private Long categoryId;
      @Column(name="item_id") private Long itemId;
      public Id() {}
      public Id(Long categoryId, Long itemId) {
         this.categoryId = categoryId;
         this.itemId = itemId;
      }
      public boolean equals(Object o) {
         if (o != null && o instanceof Id) {
            Id that = (Id)o;
            return this.categoryId.equals(that.categoryId) &&
                     this.itemId.equals(that.itemId);
         } else { return false; }
      }
      public int hashCode() {
         return categoryId.hashCode() + itemId.hashCode();
      }
   }

   @EmbeddedId private Id id = new Id();
   @Column(name="added_by_user") private String username;
   @Column(name="added_on") private Date dateAdded = new Date();
   @ManyToOne
   @JoinColumn(name="item_id", insertable=false, updatable=false)
   private Item item;
   @ManyToOne
   @JoinColumn(name="category_id", insertable=false, updatable=false)
   private Category category;
   public CategorizedItem() {}
   public CategorizedItem(String username, Category category, Item item) {
      this.username = username;
      this.category = category;
      this.item = item;
      this.id.categoryId = category.getId();
      this.id.itemId = item.getId();
      category.getCategorizedItems().add(this);
      item.getCategorizedItems().add(this);
   }
   ...
}

entity 클래스는 식별자 속성을 필요로 한다. 조인 테이블의 PK는 category_id와 item_id의 복합키로 구성되어 있다. 따라서, entity 클래스는 편이를 위해서 정적인 내포 클래스로 캡슐화하여 복합키를 가진다. CategorizedItem 생성은 복합키 세팅을 같이 수행하는데, 생성자 내에 이러한 코드가 들어가 있다.

이를 매핑 XML로 표현하면 다음과 같다.
<class name="CategorizedItem" table="CATEGORIZED_ITEM" mutable="false">
   <composite-id name="id" class="CategorizedItem$Id">
      <key-property name="categoryId" acess="field" column="category_id"/>
      <key-property name="itemId" access="field" column="item_id"/>
   </composite-id>
   <property name="dateAdded" column="added_on" type="timestamp" not-null="true"/>
   <property name="username" column="added_by_user"
         type="string" not-null="true"/>
   <many-to-on name="category" column="category_id" not-null="true"
         insert="false" update="false"/>
   <many-to-on name="item" column="item_id" not-null="true"
         insert="false" update="false"/>
</class>

entity 클래스는 한번 생성되면 속성을 변경할 수 없는 immutable이다. Hibernate는 <composite-id> 필드를 직접 접근이 가능하며, 내포된 클래스에 getter나 setter가 필요하지 않다. 두개의 <many-to-one> 매핑은 insert와 update가 false로 세팅되어서 효과적으로 읽기만 가능하다. 이는 컬럼이 한번은 복합키(값에 대한 세팅시)에 다른 한번은 다대일 관계에 두번 매핑되기 때문에 필요하다.

Category와 Item entity는 CategorizedItem entity에 대해서 일대다 관계를 가진다. Category 예를 들면 다음과 같다.
<set name="categorizedItems" inverse="true">
   <key column="category_id"/>
   <one-to-many class="CategorizedItem"/>
</set>
위의 XML 매핑은 다음의 annotation과 동일한다.
@OneToMany(mappedBy="category")
private Set<CategorizedItem> categorizedItems = new HashSet<CategorizedItem>();

여기에서는 특별히 생각할 것이 없다. 일반적인 collection이 있는 양방향 일대다 관계일 뿐이다. Item도 동일한 방식으로 collection을 추가하고 관계를 맺으면 된다. 다음과 같이 category와 item 사이의 관계를 생성하고 저장한다.
CategorizedItem newLink = new CategorizedItem(aUser.getUsername(),
                                                                            aCategory, anItem);
session.save(newLink);
자바 객체에 대한 참조 무결성은 aCategory와 anItem 내의 collection 을 관리하는 CategorizedItem의 생성자에 의해서 보장된다. category와 item 사이의 관계를 제거하는 것은 다음과 같다.
aCategory.getCategorizedItems().remove(theLink);
anItem.getCategorizedItems().remove(theLink);
session.delete(theLink);

이와 같은 방법의 주요한 장점은 양방향 항해(bidirectional navigation)이 가능하다는 것이다. aCategory.getCategorizedItems() 를 통해서 category에 있는 모든 items를 접근할 수 있으며 또한 반대 방향으로 anItem.getCategorizedItems()를 통해서 접근이 가능하다. 단점은 관계를 생성하고 없애기 위해서 CategorizedItem entity 인스턴스를 관리하는 다소 복잡한 코드가 필요하다는 것이다. category와 item은 별도로 저장되고 삭제되어야 하며 CategorizedItem 클래스에 복합 식별자와 같은 특별한 코드가 필요하다. 하지만, Category와 Item의 CategorizedItem collection에 cascading 옵션을 사용해서 transitive persistence가 가능하다.

join 테이블에 추가 컬럼에 대한 두번째 방법은 중간의 entity 클래스가 필요없으며, 더 간단하다.

component의 집합으로 join 테이블 매핑
먼저 CategorizedItem 클래스를 단순화시키는데, 식별자나 복잡한 생성자가 없는 value 타입 형태로 만든다.
pulbic class CategorizedItem {
   private String username;
   private Date dateAdded = new Date();
   private Item item;
   private Category category;
   public CategorizedItem(String username, Category category, Item item) {
      this.username = username;
      this.category = category;
      this.item = item;
   }
   ...
   // getter와 setter 메소드
   // equals와 hashCode 메소드도 필요함.
}
모든 value 타입이 그렇듯이 위의 클래스도 entity에 의해서 소유된다. 소유자는 Category이며, 이 component에 대해서 collection을 가진다.
<class name="Category" table="CATEGORY">
   ...
   <set name="categorizedItems" table="CATEGORY_ITEM">
      <key column="category_id"/>
      <composite-element class="CategorizedItem">
         <parent name="category"/>
         <many-to-one name="item" column="item_id" not-null="true" class="Item"/>
         <property name="username" column="added_by_user"/>
         <property name="dateAdded" column="added_on"/>
      </composite-element>
   </set>
</class>
이는 조인 테이블에 추가 컬럼을 가지는 다대다 관계에 대한 완전한 매핑이다. <many-to-one> 요소는 Item에 대한 관계를 나타내며, <property> 매핑은 조인 테이블의 추가 컬럼을 포함한다. 데이터베이스 테이블에 단 하나의 변경이 있다. CATEGORY_ITEM 테이블은 category_id 뿐만 아니라 item_id인 모든 컬럼의 복합인 PK를 가진다. 따라서 모든 속성(여기서 many-to-one의 item_id 에 대한 설정)은 nullable 이어서는 안된다. 그렇지 않으면, 조인 테이블에 행을 식별할 수 없다.

위에 매핑에 사용자 이름 대신 User에 대한 참조를 사용해서 확장할 수 있다. 이는 조인 테이블에 USERS 에 대한 외래키로 user_id 컬럼이 추가로 필요하다. 이러한 관계를 ternary association 매핑이라고 한다.
<set name="categorizedItems" table="CATEGORY_ITEM">
   <key column="category_id"/>
   <composite-element class="CategorizedItem">
      <parent name="category"/>
      <many-to-one name="item" column="item_id" not-null="true" class="Item"/>
      <many-to-one name="user" column="user_id" not-null="true" class="User"/>
      <property name="datedAdded" column="added_on"/>
   </composite-element>
</set>
얼마나 환상적인가!

component의 collection을 사용하는 장점은 연결 객체의 명확한 내재된 생명주기이다. Category와 Item 간의 관계를 생성하기 위해서 collection에 새로운 CategorizedItem 인스턴스를 추가하면 된다. 연결을 끊으려면 collection에서 해당 요소를 없앤다. 어떤 cascading 세팅이 필요하지 않으며, Java 코드는 다음과 같이 단순해진다.
CategorizedItem aLink = new CategorizedItem(aUser.getUserName(),
                                                                   aCategory, anItem);
aCategory.getCategorizedItems().add(aLink);
aCategory.getCategorizedItems().remove(aLink);

이와 같은 방법의 단점은 양방향 항해를 할 수 있는 방법이 없다는 것이다. CategorizedItem 과 같은 component는 정의를 통해 참조를 공유하지 못한다. Item에서 CategorizedItem으로 항해할 수 없다. 하지만, 필요한 객체를 조회하는 쿼리를 작성할 수 있다.

동일한 매핑을 annotation을 사용해보자. 먼저, component 클래스를 @Embeddable로 만들고, component 컬럼과 관계 매핑을 추가한다.
@Embeddable
public class CategorizedItem {
   @org.hibernate.annotations.Parent
   private Category category;

   @ManyToOne
   @JoinColumn(name="item_id", nullable=false, updatable=false)
   private Item item;

   @ManyToOne
   @JoinColumn(name="user_id", nullable=false, updatable=false)
   private User user;

   @Temporal(TemporalType.TIMESTAMP)
   @Column(name="added_on", nullable=false, updatable=false)
   private Date dateAdded;
   ...
   // 생성자
   // getter와 setter 메소드
   // equals와 hashCode 메소드로 필요함.
}

Category 클래스에 component의 collection으로 매핑하면 다음과 같다.
@org.hibernate.annotations.CollectionOfElements
@JoinTable(
      name="CATEGORY_ITEM",
      joinColumns=@JoinColumn(name="category_id")
)
private Set<CategorizedItem> categorizedItems = new HashSet<CategorizedItem>();
이제 annotation을 사용한 ternary association이 매핑되었다. 처음에 복잡하게 보였던 것이 단 몇 줄의 annotation 메타데이터를 사용해서 줄어들었다.

JAVA Persistence With Hibernate 상세보기
Bauer, Christian/ King, Gavin 지음 | Oreilly & Associates 펴냄
반응형