새로운 사용자들이 Hibernate로 행하고자 시도하는 바로 첫 번째 것들 중 하나는 부모/자식 타입의 관계를 모형화 시키는 것이다. 이것에 대한 두 가지 다른 접근법들이 존재한다. 여러가지 이유들로 인해 특히 새로운 사용자들에게 가장 편한 접근법은 Parent로부터 Child로의 <one-to-many> 연관을 가진 엔티티 클래스들로서 Parent와 Child 양자를 모형화 시키는 것이다. (다른 접근법은 Child를 <composite-element>로 선언하는 것이다.) 이제, (Hibernate에서) one to many 연관에 대한 디폴트 의미는 composite 요소 매핑의 의미보다 부모/자식 관계의 통상적인 의미에 훨씬 덜 가깝다는 것이 판명된다. 우리는 부모/자식 관계를 효율적이고 강력하게 모형화 시키기 위해 케스케이드들을 가진 양방향 one to many 연관을 사용하는 방법을 설명할 것이다. 그것은 전혀 어렵지 않다!
Hibernate 콜렉션들은 그것들의 소유하고 있는 엔티티의 논리적 부분으로 간주된다; 결코 포함된 엔티티들의 부분이 아니다. 이것은 중대한 구분점이다! 그것은 다음은 다음 결과들을 갖는다:
콜렉션으로부터 객체를 제거하고/콜렉션에 객체를 추가 시킬 때, 콜렉션 소유자의 버전 번호가 증가된다.
만일 콜렉션으로부터 제거되었던 객체가 하나의 값 타입의 인스턴스(예를 들어 composite 요소)이면, 그 객체는 영속상태를 끝내고 그것의 상태가 데이터베이스로부터 완전히 제거될 것이다. 마찬가지로 하나의 값 타입의 인스턴스를 콜렉션에 추가시키는 것은 그것의 상태가 즉시 영속화 되도록 강제시킬 것이다.
반면에, 만일 엔티티가 콜렉션으로부터 제거될 경우(one-to-many 또는 many-to-many 연관), 그것은 디폴트로 삭제되지 않을 것이다. 이 특징은 완전하게 일관적이다 - 다른 엔티티의 내부 상태에 대한 변경은 연관된 엔티티를 사라지도록 강제하지 않을 것이다! 마찬가지로 콜렉션에 엔티티를 추가시키는 것은 디폴트로 그 엔티티가 영속화 되도록 강제시키지 않는다.
대신에 콜렉션으로의 엔티티 추가가 두 엔티티들 사이에 단지 하나의 링크를 생성시키는 반면에, 그것을 제거하는 것은 링크를 제거한다는 점이 디폴트 특징이다. 이것은 모든 종류의 경우들에 대해 매우 적절하다. 그것이 전혀 적절하지 않은 곳은 부모/자식 관계인 경우이고, 여기서 자식의 생애는 부모의 생명주기에 묶여져 있다.
Parent로부터 Child로의 간단한 <one-to-many> 연관관계로 시작한다고 가정하자.
<set name="children"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
우리가 다음 코드를 실행시켰다면
Parent p = .....; Child c = new Child(); p.getChildren().add(c); session.save(c); session.flush();
Hibernate는 두 개의 SQL 문장들을 실행할 것이다:
c에 대한 레코드를 생성시키는 INSERT
p로부터 c로의 링크를 생성시키는 UPDATE
이것은 비효율적일 뿐만 아니라, 또한 parent_id 컬럼 상의 임의의 NOT NULL 컨스트레인트에 위배된다. 우리는 콜렉션 매핑에서 not-null="true"를 지정함으로써 null 허용 가능 컨스트레인트 위반을 정정할 수 있다:
<set name="children"> <key column="parent_id" not-null="true"/> <one-to-many class="Child"/> </set>
하지만 이것은 권장되는 해결책이 아니다.
이 행위의 기본 원인은 p로부터 c로의 링크(foreign key parent_id)가 Child 객체의 상태의 부분으로 간주되지 않고 그러므로 INSERT로 생성되지 않는다는 점이다. 따라서 해결책은 Child 매핑의 링크 부분을 만드는 것이다.
<many-to-one name="parent" column="parent_id" not-null="true"/>
(우리는 또한 parent 프로퍼티를 Child 클래스에 추가시킬 필요가 있다.)
이제 Child 엔티티가 링크의 상태를 관리한다는 점을 노트하고, 우리는 링크를 업데이트 시키지 말도록 콜렉션에게 통보한다. 우리는 inverse 속성을 사용한다.
<set name="children" inverse="true"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
다음 코드는 새로운 Child를 추가시키는데 사용될 것이다
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); c.setParent(p); p.getChildren().add(c); session.save(c); session.flush();
그리고 이제, 유일하게 한 개의 SQL INSERT가 실행될 것이다!
약간 거칠게, 우리는 Parent의 addChild() 메소드를 생성시킬 수 있다.
public void addChild(Child c) { c.setParent(this); children.add(c); }
이제, Child를 추가하는 코드는 다음과 같다
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.save(c); session.flush();
save()에 대한 명시적인 호출은 여전히 성가시다. 우리는 케스케이딩을 사용하여 이것을 얘기할 것이다.
<set name="children" inverse="true" cascade="all"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
다음은 위의 코드를 단순화 시킨다
Parent p = (Parent) session.load(Parent.class, pid); Child c = new Child(); p.addChild(c); session.flush();
유사하게, 우리는 Parent를 저장하거나 삭제할 때 자식들에 대해 반복하는 것을 필요로 하지 않는다. 다음은 데이터베이스로부터 p와 모든 그것의 자식들을 제거시킨다.
Parent p = (Parent) session.load(Parent.class, pid); session.delete(p); session.flush();
하지만, 다음 코드
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); c.setParent(null); session.flush();
는 데이터베이스로부터 c를 제거하지 않을 것이다; 그것은 오직 p에 대한 링크만을 제거할 것이다(그리고 이 경우에 NOT NULL 컨스트레인트 위반을 일으킬 것이다 ). 당신은 명시적으로 Child를 delete() 시킬 필요가 있다.
Parent p = (Parent) session.load(Parent.class, pid); Child c = (Child) p.getChildren().iterator().next(); p.getChildren().remove(c); session.delete(c); session.flush();
이제 우리의 경우에 Child는 그것의 부모 없이는 진정으로 존재할 수 없다. 따라서 만일 우리가 콜렉션으로부터 하나의 Child를 제거할 경우, 우리는 그것이 정말로 삭제되기를 원한다. 이를 위해 우리는 cascade="all-delete-orphan"을 사용해야 한다.
<set name="children" inverse="true" cascade="all-delete-orphan"> <key column="parent_id"/> <one-to-many class="Child"/> </set>
노트: 비록 콜렉션 매핑이 inverse="true"를 지정할 지라도, 케스케이드들은 여전히 콜렉션 요소들을 반복함으로써 처리된다. 따라서 객체가 케스케이드에 의해 저장되고, 삭제되거나 업데이트 되는 것을 당신이 필요로 할 경우, 당신은 그것을 그 콜렉션에 추가해야 한다. 단순히 setParent()를 호출하는 것으로는 충분하지 않다.
우리가 하나의 Session 속에 Parent를 로드시켰고 UI 액션에서 어떤 변경들을 행했고, update()를 호출하여 새로운 세션에서 이들 변경들을 영속화 시키는 것을 원한다고 가정하자. Parent는 자식들을 가진 콜렉션을 포함할 것이고, 케스케이딩 업데이트가 사용 가능하기 때문에, Hibernate는 어느 자식들이 새로이 초기화 되는지 그리고 어느 것이 데이터베이스에서 현재 행들을 표현하는지를 알 필요가 있다. Parent와 Child 모두 Long 타입의 식별자 프로퍼티들을 생성시켰다고 가정하자. Hibernate는 어느 자식들이 새로운 것인지를 결정하는데 식별자와 version/timestamp 프로퍼티 값을 사용할 것이다.(10.7절. “자동적인 상태 검출”을 보라.) Hibernate3에서는unsaved-value를 더이상 명시적으로 지정할 필요가 없다.
다음 코드는 parent와 child를 업데이트하고 newChild를 삽입시킬 것이다.
//parent and child were both loaded in a previous session parent.addChild(child); Child newChild = new Child(); parent.addChild(newChild); session.update(parent); session.flush();
물론 그것은 생성되는 식별자의 경우에는 모두 매우 좋지만, 할당되는 식별자들과 composite 식별자들에 대해서는 어떠한가? 이것은 보다 어렵다. 왜냐하면 Hibernate는 (사용자에 의해 할당된 식별자를 가진) 새로이 초기화 된 객체와 이전 세션에서 로드되었던 객체 사이를 구별짓는데 식별자 프로퍼티를 사용할 수 없기 때문이다. 이 경우에, Hibernate는 timestamp 프로퍼티 또는 version 프로퍼티를 사용하거나 실제로 second-level 캐시를 질의하거나 가장 나쁜 경우에는 행이 존재하는지를 알기 위해 데이터베이스를 질의할 것이다.
여기에 숙지할 것이 약간 있고 그것은 처음에는 혼동스러운 것처럼 보일 수 있다. 하지만 실제로 그것은 모두 매우 좋게 동작한다. 대부분의 Hibernate 어플리케이션들은 많은 장소들에서 부모/자식 패턴을 사용한다.
우리는 첫 번째 단락에서 대안을 언급했다. 위의 쟁점들 중 어느 것도 정확하게 부모/자식 관계의 의미를 가진, <composite-element> 매핑들의 경우에는 존재하지 않는다. 불행히도, composite 요소 클래스들에 대한 두 개의 커다란 제약들이 존재한다: composite 요소들은 콜렉션들을 소유하지 않고, 그것들은 유일한 부모가 아닌 다른 어떤 엔티티의 자식일 수는 없다.