Bi-Directional

기존 Uni-Directional 에서는 Instructor 를 load 해야만 그에 대한 InstructorDetail 을 가져올 수 있었다.

그러나 Bi-Directional relationship 을 사용하면 InstructorDetail 을 load 함으로써 이와 associated 된 Instructor 를 get 할 수 있다.

Uni-Directional 에서 Bi-Directional 로 바꾸는 것은 단순히 Java code 만 수정하면 된다. 기존의 DB schema 를 수정할 필요도 없다.

Bi-Directional One-to-One Mapping 의 Development Process 는 다음과 같다.

  1. Make updates to InstructorDetail class
  2. Add new feild to reference Instructor with @OneToOne annotation
  3. Add getter/setter methods for Instructor
  4. Create Main App
1.1. Add new feild to reference Instructor with @OneToOne annotation
@OneToOne(mappedBy = "instructorDetail", cascade = CascadeType.ALL)  
private Instructor instructor;

@OneToOne annotation 을 통하여 위와 같이 field 를 설정하면 된다. 또한 mapping 된 다른 entity class 의 어떤 field 와 연관이 되어있는지를 mappedBy 를 통하여 설정할 수 있다. 여기서는 Instructor class 의 instructorDetail field 와 연관된 것이다.

1.2. Add getter/setter methods for Instructor

이후에 instructor 의 getter 와 setter 를 추가한다. 이때, toString() method 에는 추가된 instructor field 에 대한 내용을 추가하면 안 된다.

만약 디버깅이나 기타 출력을 위하여 entity class object 자체를 출력하는 상황에서, owning side(FK 를 보유한 측, 주인) 와 inverse side(비주인) 모두 서로의 object 가 모두 각각의 toString() method 에 포함되어 있다면 Infinite Recursion(무한 재귀 호출) 또는 Circular Reference(순환 참조) 문제가 발생하게 된다.

따라서 실제 FK 를 보유한 owning side 에서만 toString() 에 추가하는 것이 바람직한 방법인 것 같다.

2. Create Main App
private void findInstructorDetail(AppDAO appDAO) {  
    int theId = 2;  
    InstructorDetail tempInstructorDetail = appDAO.findInstructorDetailById(theId);  
  
    System.out.println("tempInstructorDetail: " + tempInstructorDetail);  
    System.out.println("the associated instructor: " + tempInstructorDetail.getInstructor());  
  
    System.out.println("Done! :)");  
}

이번에는 Bi-Directional One-To-One mapping 을 test 하기 위하여 위와 같이 InstructorDetail 을 통하여 Instructor data 를 읽어오도록 code 를 작성한다.

결과적으로 위와 같이 성공적으로 Instructor object 를 성공적으로 가져온 것을 볼 수 있다.

More on mappedBy

mappedBy 의 동작 방식에 대하여 자세하게 알아보자.

mappedBy 를 사용하게 되면, Hibernate 는 다음과 같이 동작한다.

  1. InstructorDetail class 가 연관관계의 주인이 아니라는 걸 인식
  2. mappedBy = "instructorDetail" 라고 했으니까, Instructor class 안에서 instructorDetail 라는 field 를 검색
  3. 해당 field 에는 @JoinColumn(name = "instructor_detail_id") 가 붙어있기 때문에, Hibernate는 “아, 실제 FK 는 instructor 테이블에 있구나”라고 판단
  4. 따라서 Hibernate는 InstructorDetail 테이블에는 아무 외래 키도 만들지 않음, 단순히 mapping 만 참조

결과적으로 mappedBy 는 Hibernate 에게 “FK 는 이쪽말고 저쪽에 있어, 난 그냥 참고만 하는거야”라고 알려주는 역할이고, Hibernate 는 주인 쪽 enitity 에서 FK 정보(@JoinColumn)를 찾아 사용한다.

One-to-One Mapping Bi-Directional - Test (Delete)

이번에는 Delete 를 통하여 Bi-Directional 를 test 해보자.

@Override  
@Transactional  
public void deleteInstructorDetailById(int theId) {  
    InstructorDetail tempInstructorDetail = entityManager.find(InstructorDetail.class, theId);  
    entityManager.remove(tempInstructorDetail);  
}

위와 같이 AppDAOImpl 에 method 를 추가하고,

private void deleteInstrcutorDetail(AppDAO appDAO) {  
    int theId = 2;  
    System.out.println("Deleting instructor detail id: " + theId);   
    appDAO.deleteInstructorDetailById(theId);  
    System.out.println("Done!");  
}

Main App 에서도 method 를 추가하여 test 해보자.

위에서 InstructorDetail entity class 의 Instructor field 에도 Cascading option 을 ALL 로 걸어주었으니, InstructorDetail 을 delete 하면 그 작업이 Instructor 에도 흘러가 모두 delete 이 될 것이다. Main App 을 실행하여 결과를 확인하자.

성공적으로 실행된 것을 볼 수 있다.

One-to-One Mapping Bi-Directional - Test (Only delete Detail)

그럼 One-to-One Mapping Bi-Direction 의 상황에서, 오직 InstructorDetail 만 삭제하고 싶을 때에는 어떻게 해야 할까?

우선 말로 설명해보면 다음과 같다. 먼저 InstructorDetailinstructor field 에 설정한 Cascading option 을 조정해야 한다. 아래와 같이 ALL 에서 CascadeType.REMOVE 만 빼고 전부 다 넣어주면 될 것이다.

@OneToOne(mappedBy = "instructorDetail", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})  
private Instructor instructor;

그러나 아직 Instructor object 의 instructorDetail field 가 InstructorDetailid field 를 참조하고 있기 때문에 InstructorDetail object 를 먼저 delete 할 경우,

위와 같이 다른 exception 을 발생시키지는 않으나 Hibernate 가 SELECT query 만 실행할 뿐, DELETE query 는 내부적으로 실행조차 하지 않는다.

이유는 JPA 의 Persistence Context 의 관리 방식과 연관관계의 소유권 개념 때문이다.

아무리 Bi-Directional 이라고 하더라도 mappedBy 로 관리되는 쪽은 Non-Owning Side 이며, 이는 단순히 연관관계에서 Read-Only View 일 뿐이며, 변경 사항이 DB 에 직접 반영되지 않는다.

따라서 InstructorDetail 만 delete 하기 위하여는 해당 object 를 완전한 독립적 object 로 놔두어야 한다. 즉, 다른 object 로부터 참조를 받는 상태이면 안 된다.

@Override  
@Transactional  
public void deleteInstructorDetailById(int theId) {
	...
    tempInstructorDetail.getInstructor().setInstructorDetail(null);  
    ...
}

그렇다면 위와 같이 InstructorDetail 에 associate 된 Instructor 의 참조를 강제로 null 로 바꾸어 Bi-Directional 관계를 끊어야 한다. 이렇게 하고 다시 test 해보면,

Hibernate 가 null 로 바꾸는 query 를 실행하고, 아래에 DELETE query 도 정상적으로 보낸 것을 확인할 수 있다.

실제 DB 에서도 Instructor 은 유지되어 있는 것을 볼 수 있다.