Spring/팀스파르타

36. Entity 연관 관계 - 1 대 1

열심히 해 2024. 12. 6. 14:54

@OneToOne

  • @OneToOne 애너테이션은 1 대 1 관계를 맺어주는 역할을 합니다.
  • 고객 Entity와 음식 Entity가 1 대 1 관계라 가정하여 관계를 맺어보겠습니다.

 

 

단방향

 

외래 키의 주인 정하기

  • Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계인 Entity 이지만 1 대 1 관계에서는 외래 키의 주인을 직접 지정해야합니다.
  • 외래 키 주인만이 외래 키를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능합니다.
  • @JoinColumn()은 외래 키의 주인이 활용하는 애너테이션입니다.
    • 컬럼명, null 여부, unique 여부 등을 지정할 수 있습니다.

 

음식 Entity가 외래 키의 주인인 경우

 

 

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

 

 

 

고객 Entity가 외래 키의 주인인 경우

 

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}

 

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "food_id")
    private Food food;
}

 

 

더보기
  1. 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능합니다.
    • 다만 1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성됩니다.
    • 따라서 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 활용하시는게 좋습니다.

  2. 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA가 외래 키의 주인 Entity를 파악할 수가 없어 의도하지 않은 중간 테이블이 생성되기 때문에 반드시 설정해주시는게 좋습니다.

 

 

 


양방향 

 

 

음식 Entity가 외래 키의 주인인 경우!

 

 

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne(mappedBy = "user")
    private Food food;
}

 

 

고객 Entity가 외래 키의 주인인 경우!

 

 

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne(mappedBy = "food")
    private User user;
}

 

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "food_id")
    private Food food;
}

 

 

양방향 관계에서 외래 키의 주인을 지정해 줄 때 mappedBy 옵션을 사용합니다.

  • mappedBy의 속성값은 `외래 키의 주인인 상대 Entity의 필드명`을 의미합니다.
    >>
    public class Food {
       //...
       private final User user;   // 여기 **user**를 의미합니다.
    }

 

 

 

관계 설정 방법에 대해 정리 해보겠습니다.

  • 단방향이라면 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정해주면됩니다.
  • 양방향이라면 외래 키의 주인은 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정을 해줍니다.
    • 그리고 상대 Entity는 외래 키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션을 사용하여 속성 값으로 외래 키의 주인 Entity에 선언된 @JoinColumn()으로 설정되고 있는 필드명을 넣어주면 됩니다.

 

 

 


 

 

'외래 키 주인만이 외래 키를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능합니다.'의 의미

 

 

전제: 음식 Entity가 외래 키의 주인이고, 고객 Entity와 양방향 관계이다.

@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트")
void test4() {
    User user = new User();
    user.setName("Robbert");

    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);
    food.setUser(user); // 외래 키의 주인인 food에서 user 저장 -> 가능

    userRepository.save(user);
    foodRepository.save(food);
}

@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패")
void test2() {
    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
    User user = new User();
    user.setName("Robbie");
    user.setFood(food); // 외래 키의 주인이 아닌 user 에서 food를 저장 -> 실패 원인

    userRepository.save(user);
    foodRepository.save(food);

    // 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}

@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {
    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장하기 위해 addFood() 메서드 추가
    // 외래 키(연관 관계) 설정 food.setUser(this); 추가
    User user = new User();
    user.setName("Robbie");
    user.addFood(food);

    userRepository.save(user);
    foodRepository.save(food);
    
    
/*
@Entity
public class User {
//...
    public void addFood(Food food) {
        this.food = food;
        food.setUser(this);
    }
}
*/
}

 

 

 

 

양쪽에서 조회 가능

@Test
@DisplayName("1대1 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

@Test
@DisplayName("1대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    Food food = user.getFood();
    System.out.println("food.getName() = " + food.getName());
    System.out.println("food.getPrice() = " + food.getPrice());
}