Spring

Spring 숙련 16 (1 대 1 관계) (N 대 1 관계)

sehunbang 2024. 1. 29. 19:25

 

1 대 1 관계

@OneToOne :  1 대 1 관계를 맺어주는 역할을 합니다.

 

고객 Entity와 음식 Entity가 1 대 1 관계라 가정하여 관계를 맺어보겠습니다.

@JoinColumn() 외래 키의 주인이 활용하는 애너테이션입니다.

( 컬럼명, null 여부, unique 여부 등을 지정할 수 있습니다. )

 

 

단방향 관계:

Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계인 Entity 이지만 1 대 1 관계에서는 외래 키의 주인을 직접 지정해야합니다.

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

Food 

@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;
}

 

User

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

 

테스트

// 외래 키의 주인인 Food Entity user 필드에 user 객체를 추가해 줍니다.
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.setUser(user); // 외래 키(연관 관계) 설정

 

 

양방향 관계 :

양방향 설정 :

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

mappedBy의 속성값은 외래 키의 주인인 상대 Entity의 필드명을 의미합니다.

 

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

 

( 단방향이라면 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정해주면됩니다. )

 

! 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능합니다.

다만 1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 을 생략한다면

JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성됩니다.

 

+ Jpa 는 @mapedBy()  가 없으면 외래키 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;
}

 

테스트

@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);

    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);
}

@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); // 외래 키(연관 관계) 설정

    UserRepository.save(user);
    FoodRepository.save(food);
}

08. N 대 1 관계

@ManyToOne 애너테이션은 N 대 1 관계를 맺어주는 역할을 합니다.

음식 Entity와 고객 Entity가 N1 관계라 가정하여 관계를 맺어보겠습니다.

 

음식 Entity가 N의 관계로 외래 키의 주인 :

단방향 관계 :

 

Food

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

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

 

User

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

 

테스트

@Transactional
@SpringBootTest
public class OneToOneTest {

    @Autowired
    userRepository UserRepository;
    @Autowired
    foodRepository FoodRepository;
    @Test
    @Rollback(value = false) // 테스트에서는 @Transactional 에 의해 자동 rollback 됨으로 false 설정해준다.
    @DisplayName("1대1 단방향 테스트")
    void test1() {

        User user = new User();
        user.setName("Robbie");

        // 외래 키의 주인인 Food Entity user 필드에 user 객체를 추가해 줍니다.
        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);
        food.setUser(user); // 외래 키(연관 관계) 설정

        UserRepository.save(user);
        FoodRepository.save(food);
    }


}

 

 

양방향 관계 :

 

고객 Entity에서 Java 컬렌션을 사용하여 음식 Entity 참조.

User 에

addFoodList() 함수 추가

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

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this); // 외래 키(연관 관계) 설정
    }
}

 

 

테스트 :

@Transactional
@SpringBootTest
public class ManyToOne{
@Autowired
userRepository userRepository;
@Autowired
foodRepository foodRepository;
@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패")
void test2() {

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);

    Food food2 = new Food();
    food2.setName("양념 치킨");
    food2.setPrice(20000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
    User user = new User();
    user.setName("Robbie");
    user.getFoodList().add(food);
    user.getFoodList().add(food2);

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

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

@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);

    Food food2 = new Food();
    food2.setName("양념 치킨");
    food2.setPrice(20000);

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

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

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

    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);
    food.setUser(user); // 외래 키(연관 관계) 설정

    Food food2 = new Food();
    food2.setName("아보카도 피자");
    food2.setPrice(50000);
    food2.setUser(user); // 외래 키(연관 관계) 설정

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

 

 

조회

@Transactional
@SpringBootTest
public class ManyToOne_Search{
    @Autowired
    userRepository userRepository;
    @Autowired
    foodRepository foodRepository;
    @Test
    @DisplayName("N대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("N대1 조회 : User 기준 food 정보 조회")
    void test6() {
        User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
        // 고객 정보 조회
        System.out.println("user.getName() = " + user.getName());

        // 해당 고객이 주문한 음식 정보 조회
        List<Food> foodList = user.getFoodList();
        for (Food food : foodList) {
            System.out.println("food.getName() = " + food.getName());
            System.out.println("food.getPrice() = " + food.getPrice());
        }
    }
}

 

Hibernate: 
    select
        u1_0.id,
        u1_0.name 
    from
        users u1_0 
    where
        u1_0.id=?
user.getName() = Robbie
Hibernate: 
    select
        fl1_0.user_id,
        fl1_0.id,
        fl1_0.name,
        fl1_0.price 
    from
        food fl1_0 
    where
        fl1_0.user_id=?