본문 바로가기

[부트캠프] kdt 심화 과정

[스파르타코딩] 숙련 주차(2)

단방향 관계 설정 -- 음식 Entity에서만 User 객체에 대한 정보를 조회할 수 있다.

@ManyToOne은 N대1(다대일) 관계를 맺어주는 애너테이션이다.

음식 Entity와 고객 Entity가 N대1 관계라 가정할 때,

(다른 종류의 음식) 여러 개의 음식을 한 사람이 주문할 수 있다?

→ 고객 Entity의 입장에서 한 사람이 여러 개의 음식을 주문할 수 있다.

인스턴스를 생각한다면 종류가 같더라도 생성 시점에 따라 서로 다른 객체로 식별,

서로 다른 객체로 식별하는 같은 종류의 음식 Entity 인스턴스 여러 개가 한 사람에게 주문될 수 있다.

 

음식 Entity를 외래키의 주인으로 설정하려면

→ 음식 Entity가 고객 Entity의 주키를 외래키로 갖는 상황 

→ 음식 Entity를 통해 해당 음식을 어떤 고객이 주문했는지 조회/삽입/수정이 가능한 상황

@JoinColumn 애너테이션을 통해 참조할 상대 Entity 필드를 설정함,

상대 Entity의 주키가 @JoinColumn 애너테이션 속성인 name에 설정한 값(user_id)에 삽입됨

* @JoinColumn 애너테이션을 갖는 Entity가 외래키의 주인이다.

@Entity // JPA가 관리하는 Entity 클래스임을 설정
@Table(name="food") // @Table 애너테이션, 속성 name 설정으로 클래스명과 다른 테이블명을 설정할 수 있음
@Setter
@Getter
public class Food {
	@Id // primary key 설정
    @GeneratedValue(strategy=GenerationType.IDENTITY) // auto_increment 설정
    private Long id; // 음식 Entity 객체를 식별하는 id, Long --> bigint
    
    private String name; // 음식명
    private double price; // 음식 가격
    
    @ManyToOne // 다대일 관계 설정
    // User타입 user를 참조하는 값을 user_id에 삽입될 것을 설정
    // @JoinColumn 애너테이션을 갖는 Entity 클래스가 외래키의 주인이 된다.
    // 외래키의 주인이 된다는 것은 해당(참조 필드 타입 --> User) 객체에 대한 정보를
    // 이 Entity 객체(Food)에서 조회할 수 있다는 것을 의미한다. 
    @JoinColumn(name="user_id") 
    private User user; // User타입 user
}

 

 

양방향 관계 설정

음식 Entity에서 고객 Entity의 정보를 조회할 수 있고,

고객 Entity에서 음식 Entity의 정보를 조회할 수 있다.

양방향 참조를 위해 고객 Entity에서 Java 컬렉션을 사용하여 음식 Entity를 참조

(음식 Entity가 N의 관계로 외래키의 주인, 보통 N:1에서 N에 해당하는 Entity가 외래키의 주인이 된다)

@Entity
@Table(name="users")
@Setter
@Getter
public class User {
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    // 음식 Entity가 외래키의 주인일 때 -----------
    
    // @OneToMany: 고객 한 명이 여러 개의 음식을 주문할 수 있다.
    // mappedBy는 (외래키의 주인이 아닌 Entity에서) 외래키의 주인을 설정할 때 사용하는 옵션이다.
    // --> mappedBy에 설정된 값은("user") 상대 Entity에 설정된 자기 자신 Entity의 설정명이다.
    // Food Entity가 User타입을 외래키로 갖으며(Food Entity가 외래키의 주인)
    // Food Entity에 설정된 User 타입의 필드명을 mappedBy의 값에 설정한다.
    
    @OneToMany(mappedBy="user")
    private List<Food> foodList = new ArrayList<>();
    
    /*
    	외래키의 주인이 아닌 User에서 Food를 쉽게 저장하기 위해
        addFoodList() 메서드를 생성하고,
        해당 메서드에 외래 키(연관 관계) 설정 food.setUser(this); 추가    
    */
    public void addFoodList(Food food) {
    	this.foodList.add(food);
        food.setUser(this); // 외래키(연관관계) 설정
    }
}

@ManyToOn 코드 실습

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;

import java.util.List;
import java.util.logging.Logger;

@SpringBootTest
@Transactional
public class ManyToOneTest {

    @Autowired
    private FoodRepository foodRepository;
    @Autowired
    private UserRepository userRepository;


    @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를 쉽게 저장하기 위해
        // addFoodList() 메서드를 생성하고, 해당 메서드에 외래키(연관 관계) 설정 food.setUser(this); 추가
        User user = new User();
        user.setName("박성문");
        user.addFoodList(food);
        user.addFoodList(food2);

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

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

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


    }
    
    @Test
    @DisplayName("N대1 조회: food Entity를 기준으로 user 정보를 조회하기")
    void test4() {
        // Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
        Food food = foodRepository.findById(1L).orElse(null);
        // 음식이름 조회
        System.out.println("food.getName(): "+food.getName());

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

    @Test
    @DisplayName("N대1 조회: User 기준 Food 정보 조회")
    void test() {
        Logger log = Logger.getLogger("User 기준 Food 정보 조회");
        User user = userRepository.findById(1L).orElse(null);
        List<Food> foodList = user.getFoodList();
        log.info("for(Food food: foodList)");
        for (Food food: foodList) {
            log.info("food.getName(): "+food.getName());
            log.info("food.getPrice(): "+food.getPrice());
        }
    }
}

1대N 관계에서 단방향 관계 설정

1대N 관계(1:N) 를 설정하는 @OneToMany 애너테이션

음식 Entity와 고객 Entity가 1:N의 관계라 가정할 때,

(한 종류의 음식은) 여러 명의 고객에게 주문될 수 있다.

1:N 관계에서 N관계의 테이블이 외래키를 가질 수 있기 때문에 외래키는

N관계인 users 테이블에 외래키 컬럼을 만들어 추가하지만

외래키의 주인인 음식 Entity를 통해 관리한다.

 

@Entity
@Table(name="food")
public class Food{
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;
    private String name;
    private double price;
    
    // 1대N 관계를 설정하는 애너테이션
    // 한 종류의 음식을 여러 고객이 주문할 수 있다.
    
    @OneToMany
    // users 테이블에 food_id 컬럼을 설정하고, food Entity가 관리한다.
    @JoinColumn(name="food_id")
    private List<User> userList = new ArrayList<>(); 
}

 

1:N 관계에서 user를 조회하는 테스트 코드 실습

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;

import java.util.List;
import java.util.logging.Logger;

@SpringBootTest
public class OneToManyTest {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private FoodRepository foodRepository;

    @Test
    @DisplayName("1대N 조회 테스트")
    void test() {
        Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
        System.out.println("food.getName() = " + food.getName());

        // 해당 음식을 주문한 고객 정보 조회
        List<User> userList = food.getUserList();
        for (User user : userList) {
            System.out.println("user.getName() = " + user.getName());
        }
    }
}

다대다(M:N) 관계 설정

@ManyToMany 애너테이션을 사용하여 다대다(N:M) 관계 설정

 

@ManyToMany 애너테이션 사용시 @JoinTable 애너테이션 사용으로 중간 테이블을 만들 수 있다.

(음식 Entity가 외래키의 주인이라는 설정하에)

// 외래키의 주인인 Food Entity
@Entity
@Getter
@Setter
@Table(name="food")
public class Food {
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
    
    @JoinTable(
        name="orders" // name 속성은 현재 Entity 객체와 조인할 테이블 이름 설정
        
        // 현재 Entity 객체의(Food) 주키(pk)와 조인할 orders 테이블의 컬럼명 설정?
        joinColumns=@JoinColumns(name="food_id"),
        
        // 현재 Food Entity 객체를 기준으로
        // 반대 위치에 있는 Entity 즉 User Entity의 주키를
        // 중간 테이블의 외래키로 설정?하는데 해당 값을 갖는 컬럼명 설정?
        inverseJoinColumns=@JoinColums(name="user_id")
    )
    @ManyToMany // Food Entity와 User Entity는 다대다 관계임을 설정
    List<User> userList = new ArrayList<>();
    
    public void addUserList(User user) {
        this.userList.add(user); // 외래키 (연관관계) 설정
        user.getFoodList().add(this);
    }
}

// User Entity
// @ManyToMany 애너테이션과 mappedBy 옵션 설정으로 양방향 관계 설정
// --> User Entity에서 Food Entity 정보를 조회할 수 있음
@Entity
@Table(name="users")
public class User {
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    @ManyToMany // M:N 관계설정
    // 외래키의 주인인 Entity에서 → Food Entity
    // 해당 Entity 타입을(User Entity) 갖는 참조변수명을 mappedBy의 값으로 작성
    (mappedBy="userList")
    List<Food> foodList = new ArrayList<>();
    
    // addFoodList() 메서드 설정
    public void addFoodList(Food food) {
    	this.foodList.add(food);
        food.getUserList().add(this); // 외래키 (연관관계) 설정
    }
}

 

Food Entity 객체로 주문 테이블을 통해 해당 food를 주문한 고객 정보를 조회할 수 있음.

JPA에 의해 중간 테이블인 orders 테이블이 생성되고, JPA에 의해 생성됐기에? pk가 없음.

Food Entity의 주키를 외래키로 갖는 food_id 컬럼과 User Entity의 주키를 외래키로 갖는 user_id 컬럼이 존재함.

@ManyToMany 애너테이션으로 다대다(M:N) 관계임을 설정하고,

→ 하나의 음식은 여러명의 고객에게 주문될 수 있고, 한 명의 고객은 여러 개의 음식을 주문할 수 있다.

상대 Entity의 타입을 필드로 가져서 해당 음식을 주문한 고객에 대한 정보를 알 수 있다.

└ List<User> userList = new ArrayList<>();


지연로딩

FetchType.LAZY는 지연로딩이다. 지연로딩은 필요한 시점에 정보를 가져온다.

FetchType.EAGER는 즉시로딩이다. 즉시로딩은 즉시 정보를 가져온다.

@OneToMany는 기본 값은 FetchType.LAZY로 필요할 때 정보 조회가 이루어진다.

└ 한 명의 고객은 여러 개의 음식을 주문할 수 있다.

@ManyToOne 설정은 기본 값이 FetchType.EAGER로 즉시 정보를 가져온다.

 

영속성전이

영속상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황을 의미한다.

// User Entity

/* cascade=CascadeType.PERSIST: 영속성 전이 설정
Food타입 객체가 리스트타입으로 쌓여있는데,
이 공간에 Food타입 객체가 추가(또는 삭제 등) 되면
기존에는 FoodRepository타입 참조변수의 save() 메서드를 통해 테이블에 변경이 발생했는데,
cascade=CascadeType.PERSIST 를 설정했으므로 UserRepository 타입 참조변수를 통해
save() 메서드를 호출하는 것만으로 FoodRepository 타입 참조변수의 save() 메서드를 호출한 결과와 같다.
*/
@OneToMany(mappedBy="user", cascade=CascadeType.PERSIST)
private List<Food> foodList = new ArrayList<>();

 

@OneToMany 애너테이션의 옵션 설정중 영속성 전이와 관련된 cascade

@OneToMany(mappedBy="user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})


mysql command line 에서 사용하는 명령어

# 데이터베이스 목록 요청
show databases;

# 데이터베이스 선택
use 데이터베이스명;

# 선택한 데이터베이스 확인
select database();

# 테이블 목록 요청
show tables;

# 테이블 삭제하려는데 제약조건 걸려있을 때, 해당 제약조건 제거하기
alter table 테이블명 drop constraint 제약조건명;

# 테이블 삭제하기
drop table 테이블명;