[TIL-SpringBoot] 연관관계

이용준·2023년 3월 10일
0
post-thumbnail
~대체로 설계가 복잡해지면 각 도메인에 맞는 테이블을 설계하고 연관관계를 설정해 조인(Join) 등의 기능으로 활용합니다. 
JPA를 사용하는 애플리케이션에서도 테이블의 연관관계를 엔티티 간의 연관관계로 표현할 수 있습니다.~
-스프링부트 핵심 가이드 中-

사전 개념

  • 연관 관계의 주인

    • 두 객체(A,B)가 양방향 관계(=단방향 관계 2개)를 맺을때, 연관 관계의 주인을 지정해야 한다.
    • 주인은 제어의 권한(외래키 비롯한 테이블 레코드의 DDL)을 갖는 실직적 관계가 어떤것인지 JPA에 알려준다.
    • 주인이 아닐경우 조회만 가능
    • 외래키가 있는 곳을 연관 관계의 주인으로 정하자(중요)
  • JoinColumn

    • 매핑할 외래키 설정
    • 주인 엔티티에서 사용
      • 연관관계의 주인외래키가 있는 곳으로 설정
      • 항상 쪽이 외래 키를 갖는다.
    • 기본값이 설정되어 자동으로 매핑하지만 의도와 다른 값이 들어갈 수 있으므로 name 속성 사용
    • 미선언시 엔티티 매핑하는 중간 테이블이 생성되며 관리 포인트가 늘어나므로 좋지 않다.(<>생략도 가능하다)
       * 사용 가능한 속성
         - name : 매핑할 외래키 설정
         - referencedColumnName : 외래키가 참조할 상태 테이블 컬럼명 지정
         - foreignKey : 제약 조건 설정
          	- unique, nullable, insertable, updatable..
  • mappedBy

    • 어떤 객체가 주인인지 표시하는 속성
    • 주인은 mappedBy 속성 사용 불가
    • 주인이 아닌 엔티티에서 mappedBy 사용해 주인 지정
    • mappedBy에 들어가는 값은 상대 엔티티에 있는 연관관계 필드명
       @OneToOne(mappedBy="ssn") 
       private Person person;
       
       > Persion 엔티티가 Ssn 엔티티의 주인이 됨.
  • 로딩

    • 즉시 로딩(Eager) : 엔티티 조회 시 연관된 엔티티(테이블)도 함께 조회
    • 지연 로딩(Lazy) : 연관관계 상관없이 해당 엔티티 값만 조회

1. 연관관계 매핑 종류와 방향

연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 것을 의미한다.
JPA에서는 연관 관계의 엔티티 객체 자체를 전체 참조한다.

  • 종류

    • One To One : 1:1(일대일)
    • One To Many : 1:N(일대다)
    • Many To One : N:1(다대일)
    • Many To Many : N:M(다대다)
  • 방향

    • 단방향 : 두 엔티티에서 한쪽의 엔티티만 참조

      • Board.getReview()처럼 참조 필요시 Board->Review 단방향 참조
      • 참조 굳이 필요 없을경우 안하면 된다.
    • 양방향 : 두 엔티티에서 서로의 엔티티 참조

      • 양쪽에서 단방향으로 매핑하는 방식
      • 단방향 먼저 매핑하고 역방향으로 객체 탐색 필요할 경우 추가

1-1. One To One

  • A는 한 개의 B만 갖는다.
  • person은 하나의 주민등록번호만 갖는다.
  • person은 주민등록번호의 번호(@Id)를 외래키로 가져가 참조 가능
    • 반대의 경우도 가능
      • 외래키를 갖는 테이블을 주 테이블이라 한다.

1-1-1. 단방향 매핑

@Entity
@Table(name="person")
@Getter
@Setter
@NoArgsConstructor
public class Person{
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTIFY)
  private Long personId;
  
  private String name;
  
  @OneToOne
  @JoinColumns(name="ssn") // SSN = social Security Number - 주민번호
  private SSN ssn;
  
 }
  • SsnRepository
    - 연관관계 매핑된 person 객체 조회 가능
    @SpringBootTest
    class SsnRepository[
      @Autowired
      private SsnRepository ssnRepository
      
      @Autowired
      private PersonRepository personRepository
      
      ~ 
      
      // 생성한 데이터 조회
      System.out.println("SavePerson : "+ ssnRepository.findById(ssn.getId()).get().getPersion());
      
      System.out.println("SaveSSN : "+ ssnRepository.findById(ssn.getId()).get());
      

1-1-2.양방향 매핑

  • 양쪽에서 단방향으로 서로 매핑

  • left join 두 번 실행되므로 비효율적(1:1시)

  • 테이블간 연관관계를 맺어 한쪽 테이블이 외래키를 갖자!(주인-mappedBy)

    @Entity
     @Table(name="person")
     @Getter
     @Setter
     @NoArgsConstructor
     public class Ssn{
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTIFY)
       private Long id;
       
       private String addres;
       
       @OneToOne
       private Person person;
     
     }
     

1-2. Many To One

  • 한쪽의 엔티티가 상대 엔티티 참조하는 상태
  • 자신을 기준으로 다중성 고려

1-2-1. 단방향 매핑

먼저 테이블에 매핑되는 엔티티(동물원-동물)클래스를 생성한다.
(앞서 생성한 Person은 사육사 엔티티로 가정한다.)

  • Zoo

    @Entity
     @Getter
     @Setter
     public class Zoo {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      private String name;
    
     }
  • Animal

    @Entity
     @Getter
     @Setter
     @ToString(exclude={"person","zoo"})
     ~
     public class Animal{
       @Id
       @GenratedValue(strategy = GenerationType.IDENTIFY)
       private Long id;
       
       private Int code;
       
       private String type;
       
       private String name;
       
       @OneToOne(mappedBy="animal")
       private Person person;
       
       // 다대일 연관관계
       @ManyToOne
       @JoinColumn(name="animal_id")
       private Zoo zoo;
       
       
     }
  • 일반적으로 외래키를 갖는 쪽이 주인 역할 수행
  • 위 경우 동물(Animal) 엔티티가 동물원(Zoo)의 주인이된다.
  • PersonRepository - Person, Animal 연관관계 테스트

    @SpringBootTest
    public class PersonRepositoryTests {
     @Autowired
     private PersonRepositroy personRepositroy;
     @Autowired
     private AnimalRepository animalRepository;
    
     @Test
     public void relationTest(){
         Person person = new Person;
         person.setName("김사육사");
    
         personRepositroy.save(person);
    
         Animal animal = new Animal();
         animal.setCode(001);
         animal.setType("사자");
         animal.setName("이거사자");
         animal.setPerson(animal);
    
         personRepositroy.save(animal);
    
         System.out.println(
             "animal : " + personRepositroy.findById(1L)
                 .orElseThrow(RuntimeException::new));
    
         System.out.println(
             "Person : "+ personRepositroy.findById(1L)
                 .orElseThrow(RuntimeException::new).getPerson());
     }
    }
    

    Animal 엔티티에서 Person 엔티티 연관관계를 맺으로 PersonRepository만으로도 Person 객체도 조회 가능

1-2-2. 양방향 매핑

  • 양쪽에서 단방향으로 매핑하는 것이 양방향 매핑이다.
  • 아래 예제는 Zoo 엔티티만 연관관계 설정한다.
// Zoo 엔티티와 Animal 엔티티의 일대다 연관관계 설정

@Entity
@Getter
@Setter
~
@ToString(exclude="zoo") // 순환 참조 방지
public class Zoo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "zoo", fetch = FetchType.EAGER)
    private List<Animal> animalList = new ArrayList<>();
    
}
  • 위 일대다 연관관계는 여러 Animal 엔티티가 Zoo 엔티티에 포함될 수 있어 Collection, List, Map 형식으로 필드를 생성한다.
  • 위처럼 @OneToMany가 붙은 쪽(animal)에서 @JoinColumn을 사용시 상대 엔티티(zoo)에 외래키가 설정된다.
  • @OneToMany fetch = Lazy(default)
  • Zoo는 Animal와의 연관관계에서 주인이 아니므로 아래처럼 zoo에 직접 추가 불가능
    zoo.getAnimalList().add(animal); // 무시된다.

1-3. One To Many

일대다 양방향 관계는 어느 엔티티 클래스도 연관관계의 주인이 될 수 없으므로 다루지 않는다.

단방향 매핑

(새로운 엔티티 생성) 동물 종류(Category:1) - 동물(Animal:N)

@Entity
@Getter
@Setter
@ToString
~
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String code;

    private String name;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name="category_id")
    private List<Animal> animals = new ArrayList<>();

}

동물 종류 엔티티에서 @OneToMany, @JoinColumn 사용시 별도 설정 없이도 일대다 단방향 연관관계가 매핑된다. 지금같은 일대다 단방향 관계의 단점은 매핑 주체가 아닌 반대 테이블에 외래키가 추가되며, 이 방식은 다대일 구조와 다르게 외래키 설정위해 다른 테이블에 대한 update 쿼리를 발생시킨다. 이러한 문제 해결을 위해서는 다대일 연관관계 사용이 권장된다.


1-4. ManyToMany

  • 실무에서 거의 사용되지 않음

  • 중간 테이블이 숨겨져 있어 사용자도 모르는 복잡한 조인의 쿼리 발생 우려

  • 한 종류의 A가 여러 B와 연결될 수 있고, 한 종류의 B가 여러 A와 연결될 수 있다.

    • 한 동물(ex-사자)이 여러 동물원에 소유될 수 있고, 한 동물원이 여러 동물을 소유할 수 있다.
  • 각 엔티티가 서로를 리스트로 갖는 구조 생성되므로 교차 엔티티(중간 테이블) 생성해 다대다 or 일대다 관계로 해소한다.

1-4-1. 단방향 매핑

  • Zoo entity
@Entity
@Getter
@Setter
@ToString(exclude = "products")
~
public class Zoo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany
    private List<Animal> animals = new ArrayList<>();

    public void addAnimal(Animal animal){
        animals.add(animal);
    }
}

위에서 다대다 연관관계는 @ManyToMany로 설정하며, 리스트를 필드로 갖는 객체에서는 외래키를 갖지 않으므로 별도 @JoinColumn은 설정하지 않아도 된다.
별도 설정이 없는 경우 테이블은 "zoo_animal"라는 이름으로 설정되며, 설정 변경시에는 @JoinColumn(name="이름")으로 변경이 가능하다.

zoo_animal 테이블은 zoo와 animal 테이블에서 id 값을 가져와 두 개의 외래키가 생성된다.

1-4-2. 양방향 매핑

  • Animal entity
@Entity
@Getter
@Setter
@ToString(exclude = {"person", "animal", "animals"})
public class Animal {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int code;

    privaet String name;

    @OneToMany(mappedBy = "animal")
    private Person person;

    @ManyToOne
    @JoinColumn(name = "zoo_id")
    private Zoo zoo;

    @ManyToMany
    private List<Zoo>zooList = new ArrayList<>();

    public void addZoo(Zoo zoo){
        this.zooList.add(zoo);
    }

}

필요에 따라 mappedBy 속성을 사용해 두 엔티티 간 연관관계 주인 설정이 가능하다. 애플리케이션 실행시 데이터베이스의 테이블 구조는 변경되지 않는데, 이는 중간 테이블이 연관관계를 설정하기 때문이다.


참고

profile
뚝딱뚝딱

0개의 댓글