티스토리 뷰

728x90

0. 양방향의 설정에서 가장 중요한 것 중 하나가 참조이다.

  0-1 삭제할 때는 관계를 맺는 enitity와의 참조도 중요한 역활을 하므로 꼼꼼하게 확인해야 한다.

 

1. One To One 관계의 테이블 사이에  Bi-directional은 실제 관계형데이터 베이스에서는 사용되지 않는다.

  1-0 즉 프로그램 상 편의로 양방향 모두 Entity를 소유하도록 하는 것이다.

 

  1-1 지난 예제처럼 강사 테이블과 강사세부정보 테이블이 1:1의 관계를 가지고 있다고 하자.

    1-1-1 강사 테이블에 instructor_detail_id라는 외래키 column이 존재한다.

    1-1-2 이 컬럼 값은 Instructor_detail 테이블의 기본키 값을 참조하고 있다.

    1-1-3 즉 강사테이블에서만 강사세부정보에 대한 정보를 가지고 있다.

    1-1-4 강사세부정보 테이블에는 어떠한 외부 테이블에 대한 데이터를 가지고 있지 않는다.

    1-1-5 저번 포스팅 Uni-Direction의 접근처럼 강사 -> 강사세부정보는 방향은 변할 수 없다.

 

    1-1-6 데이터베이스에서 강사세부정보 테이블에서 매핑된 강사가 누구인지 알기 위해서는

      1-1-6-1 강사 테이블에서 시작하여 강사세부정보 테이블을 Join하여 값을 가지고 와야 한다.

      1-1-6-2 다시 말하면 방향성은 그대로 강사 -> 강사세부정보가 된다. 

        1-1-6-2-1 이는 당연하게도 참조정보가 강사 테이블에만 있기 때문이다.

 

2. 프로그램에서 생각하면 Bi-Direction의 핵심은 어떻게 접근 방향이 세부정보-> 강사인 경우를 해결하는가이다.

  2-0 왜냐하면 기본적으로 강사 -> 세부정보는 당연한 정보로 인식되기 때문에 신경 쓸 필요가 없다.

  2-1 ORM의 Entity에서는 데이터베이스와 달리 has 속성으로 관계를 정의한다.

  2-2 세부정보 -> 강사 방향이 되러면 두 Entity의 관계에 대한 정의를 세부정보 Entity가 알아야 한다.

 

  2-3 이 정보는 @OneToOne의 mappedBy 속성을 통해 지정된다.

    2-3-2 이 정보는 관계를 가지는 Entity의 어떤 속성이 두 테이블 관계의 mapping정보를 가지고 있는지를 지정한다.

    2-3-3 mappedBy로 지정된 상대 Entity 속성의 @JoinColumn 정보를 통해

    2-3-4 어떤 instructor와 매핑되는지 hibernate는 알 수 있고 내부적으로 처리하여 테이블 정보를 가져온다.

 

3. Bi-directional 방향을 위해 강사세부정보 Entity가 매핑된 강사 테이블 정보를 가지고 있는 속성을 가져야 한다.

  3-1 세부정보 -> 강사 방향의 설정이기 때문에 강사세부정보에서도 설정이 추가 되어야 한다.

    3-1-1 아래의 소스에서 InstructorDetail Entity에 Instructor 속성이 추가되었다.

 

  3-2. 강사세부정보에서 맵핑된 강사를 가져오기 위해서 @OneToOne annotation을 지정한다.

    3-2-1 cascade 속성은 강사세부정보 -> 강사 방향으로도 어떤 transcation이 cascade되는지를 지정할 수 있다.

      3-2-1-1 아래의 경우는 CascadeType.All로 지정되어 있다.

    3-2-2 mappedBy에서 가지고 올 매핑 entity의 foreign key 속성을 명시한다.

 

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "instructor_detail")
@Getter
@Setter
@NoArgsConstructor
public class InstructorDetail {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column
  private Long id;
  
  @Column(name = "youtube_channel")
  private String youtubeChannel;
  
  @Column
  private String hobby;
  
  // This attribute should be added to make bi-directional relationship
  @OneToOne(cascade = CascadeType.ALL, mappedBy = "instructorDetail")
  private Instructor instructor;

  public InstructorDetail(String youtubeChannel, String hobby) {
    super();
    this.youtubeChannel = youtubeChannel;
    this.hobby = hobby;
  }

  @Override
  public String toString() {
    return "InstructorDetail [id=" + id + ", youtubeChannel=" + youtubeChannel + ", hobby=" + hobby + "]";
  }
}

 

4. 위에 코드에서 lombok 관련 추가 및 변경된 부분이 있다.

  4-0 이 부분은 무한 재귀호출에 의한 StackOverflow Error을 피하기 위해서이다.

  4-1 lombok의 @Data annotation은 @Getter, @Setter, @ToString을 모두 포함하고 있다.

 

  4-2 Bi-directional의 경우에 관계 있는 두 Entity 모두에게 @Data를 설정하면 toString 호출에 Recusive flow가 생긴다.

    4-2-1 즉 Instructor의 toString()은 Instructor 속성 중 InstructorDetail 속성의 toString을 호출하는데,

    4-2-2 InstructorDetail에도 Instructor 속성이 있기 때문에 InstructorDetail의 toString() 호출 시

      4-2-2-1 다시 Instructor의 toString을 호출하게 되고 끝없이 이것이 반복된다.

 

  4-3 따라서 이 문제를 해결하기 위해 둘 중의 하나의 Entity에 @Data를 제거하고 @Getter @Setter를 별도로 명시하고

    4-3-1 toString() 메소드는 재귀를 방지하기 위해 매핑 Entity를 가진 속성을 제외한 형태로 구현해야 한다.

  

  4-4 이 부분이 이해가 안되면 lombok을 사용하지 말고 setter/getter, constructor를 모두 수동으로 생성하면 된다.

 

5. 테이블의 접근을 Instructor_detail -> Instructor방향으로 하여 데이터를 가져오고 삭제하는 예제이다.

  5-1 아래의 코드를 보면 무한 재귀호출을 피하기 위한 코드 때문에 두 번의 toString을 각각의 Entity에서 호출했다.

  5-2 강사세부정보 데이터를 가지고 와서 출력하는 예제이다.

    5-2-1 이젠 강사상세정보 Entity에 강사정보가 포함되어 있다.

 

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import pe.pilseong.hibernate_mapping.entities.Instructor;
import pe.pilseong.hibernate_mapping.entities.InstructorDetail;

public class SaveEntityBi {
  public static void main(String[] args) {
    SessionFactory factory = new Configuration().configure()
        .addAnnotatedClass(Instructor.class)
        .addAnnotatedClass(InstructorDetail.class)
        .buildSessionFactory();
    
    
    Session session = factory.getCurrentSession();
    session.beginTransaction();
    
    InstructorDetail instructorDetail = session.get(InstructorDetail.class, 3L);
    
    // 관계를 가지는 두개의 Entity의 toString를 각각 호출한다.
    System.out.println("Fetched InstructorDeail :: " + instructorDetail.toString());
    System.out.println("Fetched Instructor :: " + instructorDetail.getInstructor().toString());
    
    
    session.getTransaction().commit();
    
    factory.close();
  }
}

 

  5-3 강사세부정보를 조회하여 삭제하는 예제

    5-3-1 Entity 설정 시 cascade가 all로 설정되어 있으므로 참조된 테이블 정보도 삭제된다.

    5-3-2 강사세부정보 -> 강사의 방향으로도 동일하게 처리가 가능하다.

 

    5-3-3 아래의 코드는 try - catch - finally가 추가되어 있다.

      5-3-3-1 만일 session.get에서 데이터를 가져오지 못할 경우 출력하는 부분에서 null pointer 예외가 발생한다.

      5-3-3-2 그럴 경우 session이 닫히지 않아 leak이 생기는데 이를 방지하기 위한 코드를 추가하였다.

 

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import pe.pilseong.hibernate_mapping.entities.Instructor;
import pe.pilseong.hibernate_mapping.entities.InstructorDetail;

public class DeleteEntityBi {

  public static void main(String[] args) {
    SessionFactory factory = new Configuration().configure()
        .addAnnotatedClass(Instructor.class)
        .addAnnotatedClass(InstructorDetail.class)
        .buildSessionFactory();
    
    Session session = factory.getCurrentSession();
    
    try {
      session.beginTransaction();
      
      InstructorDetail instructorDetail = session.get(InstructorDetail.class, 3L);
      System.out.println("Fetched Instructor :: " + instructorDetail.getInstructor().toString());

      session.delete(instructorDetail);
      
      session.getTransaction().commit();
      
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      session.close();
      factory.close();
    }
  }
}

 

6. 약간 예외적인 상황을 가정한다. 위와 같은 Entity설정에서

  6-1 강사세부정보만 삭제하고 강사정보는 삭제하지 않으려고 한다.

  6-2 Bi-directional 이지만 세부적인 cascade control이 필요한 부분이다.

  6-3 삭제를 제외한 Refresh, Persist 등의 연동 기능은 그대로 유지하고 싶은 경우이다.

 

  6-4 절차를 세부적으로 기록하면

    6-4-1 여전히 중요한 것은 방향이다. 강사세부정보를 삭제할 때 강사가 삭제되지 않아야 한다.

    6-4-2 강사세부정보를 delete할 경우 강사 entity는 removed state에 있는 강사정보를 참조하게 된다.

    6-4-3 따라서 참조에 문제가 발생하게 되고 null pointer 예외가 발생한다.

      6-4-3-1 removed state의 entity는 commit으로 확정되지 않았으므로

      6-4-3-2 transaction manager가 관리하고

      6-4-3-3 GC에 의해 아직 수거되지 않은 상태이다.

 

    6-4-4 이 문제를 해결하려면 강사 entity에 강사세부정보 속성을 null로 미리 설정해 두어야 한다.

      6-4-4-1 강사 entity는 현재 managed 상태에 있으므로 강사의 속성 변경은 commit 시에 반영된다.

      6-4-4-2 null설정은 더 이상 참조하지 않는다는 의미이고 관련 instructor_detail 정보는 삭제되어야 하므로

        6-4-4-2-1 명시적으로 instructorDetail entity를 delete 처리한다.

      6-4-4-3 Entity lifesycle과 cascade를 머리속으로 잘 그려야 한다.

 

  6-5 복잡해 보이지만 이 내용은 cascade를 재정의하면 단순하게 구현할 수 있다.

 

public class DeleteEntityBi {

  public static void main(String[] args) {
    SessionFactory factory = new Configuration().configure()
        .addAnnotatedClass(Instructor.class)
        .addAnnotatedClass(InstructorDetail.class)
        .buildSessionFactory();
    
    Session session = factory.getCurrentSession();
    
    try {
      session.beginTransaction();
      
      InstructorDetail instructorDetail = session.get(InstructorDetail.class, 4L);
      System.out.println("Fetched Instructor :: " + instructorDetail.getInstructor().toString());
      
//      I have to decouple two entities. cut the reference from instructor to instructor_detail
      Instructor instructor = instructorDetail.getInstructor();
      instructor.setInstructorDetail(null);

      session.delete(instructorDetail);
      
      session.getTransaction().commit();
      
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      session.close();
      factory.close();
    }
  }
}
728x90
댓글