새소식

TIL

[TIL] 230522 <Spring> JPA 한 걸음 더 나아가기

  • -

고객 (users) 테이블

create table users
(
    id   bigint not null auto_increment,
    name varchar(255),
    primary key (id)
);

음식 (food) 테이블

create table food
(
    id    bigint not null auto_increment,
    name  varchar(255),
    price float(53) not null,
    primary key (id)
);

주문 (orders) 테이블

create table orders
(
    id         bigint not null auto_increment,
    user_id    bigint,
    food_id    bigint,
    order_date date,
    primary key (id)
);

alter table orders
    add constraint orders_user_fk
        foreign key (user_id)
            references users (id);

alter table orders
    add constraint orders_food_fk
        foreign key (food_id)
            references food (id);
  • 고객 : 음식 = N : M 관계
  • N : M 관계인 테이블들의 연관 관계를 해결하기 위해 orders 테이블처럼 중간 테이블을 사용가능.
    • 고객 : 주문 = 1 : N
    • 음식 : 주문 = 1 : N

 

양방향 관계

@Entity
@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;
}
@Entity
@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<>();
}
  • 한명의 고객은 여러번 주문이 가능한 상황입니다.
    • 이를 Entity에서 여러번 가능함을 표현하기 위해 Java 컬렉션을 사용하여 List<Food> foodList = new ArrayList<>() 이처럼 표현가능
  • DB 테이블에서는 고객 테이블 기준으로 음식의 정보를 조회하려고 할 때 JOIN을 사용하여 바로 조회가 가능하지만
    고객 Entity 입장에서는 음식 Entity의 정보를 가지고 있지 않으면 음식의 정보를 조회할 방법이 없음
    • 따라서 DB 테이블에 실제 컬럼으로 존재하지는 않지만 Entity 상태에서 다른 Entity를 참조하기 위해 이러한 방법을 사용
  • 현재 음식 Entity와 고객 Entity는 서로를 참조

 

단방향 관계

@Entity
@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;
}
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
  • 음식 Entity에서만 고객 Entity를 참조가능
    • 고객 Entity에는 음식 Entity의 정보가 없기 때문에 음식 정보를 조회불가

 

  • <정리>
    • DB 테이블에서는 테이블 사이의 연관관계를 FK(외래 키)로 맺을 수 있고 방향 상관없이 조회가 가능
    • Entity에서는 상대 Entity를 참조하여 Entity 사이의 연관관계를 맺을 수 있음
    • 하지만 상대 Entity를 참조하지 않고 있다면 상대 Entity를 조회할 수 있는 방법이 없음
    • 따라서 Entity에서는 DB 테이블에는 없는 방향의 개념이 존재

 


1 대 1 관계에서는 외래 키의 주인을 직접 지정해야함

외래 키 주인만이 외래 키등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키 읽기만 가능!

  • @JoinColumn()외래 키의 주인이 활용하는 애너테이션
    • 컬럼명, null 여부, unique 여부 등을 지정가능

 

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

※ 주의할 점

  1. 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능하지만
    1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성되기 때문에 생략 안하는게 좋음
  2. 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA가 외래 키의 주인 Entity를 파악할 수가 없어 의도하지 않은 중간 테이블이 생성되기 때문에 반드시 설정

 

entity>Food

@Entity
@Getter
@Setter
@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>User

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

	// 양방향
	@OneToOne(mappedBy = "user")
	private Food food;

	// 외래 키의 주인이 아닌 User에서 Food를 저장하기 위함
	public void addFood(Food food) {
		this.food = food;
		food.setUser(this);	 // 외래 키(연관 관계) 설정
	}
}

repository>FoodRepository

public interface FoodRepository extends JpaRepository<Food,Long> {
}

repository>UserRepository

public interface UserRepository extends JpaRepository<User,Long> {
}

 

OneToOneTest

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

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


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

}

 


 

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

entity>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  //N대1 관계
	@JoinColumn(name = "user_id")
	private User user;
}

entity>User

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

	// 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위함
	public void addFoodList(Food food) {
		this.foodList.add(food);
		food.setUser(this); // 외래 키(연관 관계) 설정
	}
}

 

ManyToOneTest

@Transactional
@SpringBootTest
public class ManyToOneTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;


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

		Food food = new Food();
		food.setName("후라이드 치킨");
		food.setPrice(15000);
		food.setUser(user); // 외래 키(연관 관계) 설정

		Food food2 = new Food();
		food2.setName("양념 치킨");
		food2.setPrice(20000);
		food2.setUser(user); // 외래 키(연관 관계) 설정

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


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


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

 


 

  • 외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity가 가지고 있음
    • 1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가하지만 외래 키의 주인인 음식 Entity를 통해 관리
    • 외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점
  • 1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않음!!!
  • 1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않음
  • N 관계의 Entity인 고객 Entity에서 @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 설정할 수는 있긴 함
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

		@ManyToOne
		@JoinColumn(name = "food_id", insertable = false, updatable = false)
		private Food food;
}

 

entity>Food

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

	@OneToMany  //1대N 관계
	@JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
	private List<User> userList = new ArrayList<>();
}

entity>User

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

 

OneToManyTest

@Transactional
@SpringBootTest
public class OneToManyTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;


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

		User user2 = new User();
		user2.setName("Robbert");

		Food food = new Food();
		food.setName("후라이드 치킨");
		food.setPrice(15000);
		food.getUserList().add(user); // 외래 키(연관 관계) 설정
		food.getUserList().add(user2); // 외래 키(연관 관계) 설정

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

		// 추가적인 UPDATE 쿼리 발생을 확인할 수 있습니다.
	}


	@Test
	@DisplayName("1대N 조회 테스트")
	void test2() {
		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());
		}
	}
}

 


 

entity>Food

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

	@ManyToMany  //N대M 관계
	@JoinTable(name = "orders", // 중간 테이블 생성
		joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
		inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
	private List<User> userList = new ArrayList<>();


	// 양방향
	// addUserList() 메서드를 생성해 user 정보를 추가
	public void addUserList(User user) {
		this.userList.add(user); // 외래 키(연관 관계) 설정
		user.getFoodList().add(this);  //user 객체에 food 정보를 추가
	}
}

entity>User

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


	// 양방향
	@ManyToMany(mappedBy = "userList")
	private List<Food> foodList = new ArrayList<>();

	// 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위함
	public void addFoodList(Food food) {
		this.foodList.add(food);
		food.getUserList().add(this); // 외래 키(연관 관계) 설정
	}

}

 

ManyToManyTest

@Transactional
@SpringBootTest
public class ManyToManyTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;


	@Test
	@Rollback(value = false)
	@DisplayName("N대M 단방향 테스트")
	void test1() {

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

		User user2 = new User();
		user2.setName("Robbert");

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

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

		// 자동으로 중간 테이블 orders 가 create 되고 insert 됨을 확인할 수 있습니다.
	}


	@Test
	@Rollback(value = false)
	@DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패")
	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);

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

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

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

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

		// 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드를 생성해서 사용합니다.
		// 외래 키(연관 관계) 설정을 위해 Food 에서 userList 를 호출해 user 객체 자신을 add 합니다.
		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대M 양방향 테스트")
	void test4() {

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

		User user2 = new User();
		user2.setName("Robbert");


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

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

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

		// User 를 통해 food 의 정보 조회
		System.out.println("user.getName() = " + user.getName());

		List<Food> foodList = user.getFoodList();
		for (Food f : foodList) {
			System.out.println("f.getName() = " + f.getName());
			System.out.println("f.getPrice() = " + f.getPrice());
		}

		// 외래 키의 주인이 아닌 User 객체에 Food 의 정보를 넣어주지 않아도 DB 저장에는 문제가 없지만
		// 이처럼 User 를 사용하여 food 의 정보를 조회할 수는 없습니다.
	}

	@Test
	@Rollback(value = false)
	@DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용")
	void test5() {

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

		User user2 = new User();
		user2.setName("Robbert");


		// addUserList() 메서드를 생성해 user 정보를 추가하고
		// 해당 메서드에 객체 활용을 위해 user 객체에 food 정보를 추가하는 코드를 추가합니다. user.getFoodList().add(this);
		Food food = new Food();
		food.setName("아보카도 피자");
		food.setPrice(50000);
		food.addUserList(user);
		food.addUserList(user2);

		Food food2 = new Food();
		food2.setName("고구마 피자");
		food2.setPrice(30000);
		food2.addUserList(user);


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

		// User 를 통해 food 의 정보 조회
		System.out.println("user.getName() = " + user.getName());

		List<Food> foodList = user.getFoodList();
		for (Food f : foodList) {
			System.out.println("f.getName() = " + f.getName());
			System.out.println("f.getPrice() = " + f.getPrice());
		}
	}



	@Test
	@DisplayName("N대M 조회 : Food 기준 user 정보 조회")
	void test6() {
		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());
		}
	}

	@Test
	@DisplayName("N대M 조회 : User 기준 food 정보 조회")
	void test7() {
		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());
		}
	}
}

 

중간 테이블 orders를 직접 생성하여 관리하면 변경 발생 시 컨트롤하기 쉽기 때문에 확장성에 좋음

 

entity>Food

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


	@OneToMany(mappedBy = "food")
	private List<Order> orderList = new ArrayList<>();
}

entity>User

@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<Order> orderList = new ArrayList<>();

}

entity>Order

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

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

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

 

OrderTest

@Transactional
@SpringBootTest
public class OrderTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;
	@Autowired
	OrderRepository orderRepository;


	@Test
	@Rollback(value = false)
	@DisplayName("중간 테이블 Order Entity 테스트")
	void test1() {

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

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

		// 주문 저장
		Order order = new Order();
		order.setUser(user); // 외래 키(연관 관계) 설정
		order.setFood(food); // 외래 키(연관 관계) 설정

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

	@Test
	@DisplayName("중간 테이블 Order Entity 조회")
	void test2() {
		// 1번 주문 조회
		Order order = orderRepository.findById(1L).orElseThrow(NullPointerException::new);

		// order 객체를 사용하여 고객 정보 조회
		User user = order.getUser();
		System.out.println("user.getName() = " + user.getName());

		// order 객체를 사용하여 음식 정보 조회
		Food food = order.getFood();
		System.out.println("food.getName() = " + food.getName());
		System.out.println("food.getPrice() = " + food.getPrice());
	}

}

 

중간 테이블 Order에 주문일 컬럼 추가

@Entity
@Getter
@Setter
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

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

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

	@CreatedDate
	@Temporal(TemporalType.TIMESTAMP)
	private LocalDateTime orderDate;
}

JpaAdvanceApplication

@EnableJpaAuditing 추가

 

 


  • JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 방법을 정하게되는데 Fetch Type이라 부름
    • 종류에는 2가지가 있는데 하나는 LAZY, 다른 하나는 EAGER
    • LAZY는 지연 로딩으로 필요한 시점에 정보를 가져옴
    • EAGER는 즉시 로딩으로 이름의 뜻처럼 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옴
  • 기본적으로 @OneToMany 애너테이션은 Fetch Type의 default 값이 LAZY,
    반대로 @ManyToOne 애너테이션은 EAGER로 지정되어 있음

  • 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입이니 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미하므로
      • 따라서 효율적으로 정보를 조회하기 위해 지연 로딩이 default로 설정되어있음
  • 반대로 이름 뒤쪽이 One일 경우 해당 Entity 정보가 한 개만 들어오기 때문에 즉시 정보를 가져와도 무리가 없어 즉시 로딩이 default로 설정되어있음

 

음식 테이블과 고객 테이블이 N : 1 양방향 관계라 가정

FetchTypeTest

@SpringBootTest
public class FetchTypeTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;

	@Test
	@Transactional
	@Rollback(value = false)
	void init() {
		List<User> userList = new ArrayList<>();
		User user1 = new User();
		user1.setName("Robbie");
		userList.add(user1);

		User user2 = new User();
		user2.setName("Robbert");
		userList.add(user2);
		userRepository.saveAll(userList);

		List<Food> foodList = new ArrayList<>();
		Food food1 = new Food();
		food1.setName("고구마 피자");
		food1.setPrice(30000);
		food1.setUser(user1); // 외래 키(연관 관계) 설정
		foodList.add(food1);

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

		Food food3 = new Food();
		food3.setName("후라이드 치킨");
		food3.setPrice(15000);
		food3.setUser(user1); // 외래 키(연관 관계) 설정
		foodList.add(food3);

		Food food4 = new Food();
		food4.setName("후라이드 치킨");
		food4.setPrice(15000);
		food4.setUser(user2); // 외래 키(연관 관계) 설정
		foodList.add(food4);

		Food food5 = new Food();
		food5.setName("고구마 피자");
		food5.setPrice(30000);
		food5.setUser(user2); // 외래 키(연관 관계) 설정
		foodList.add(food5);
		foodRepository.saveAll(foodList);
	}

	@Test
	@DisplayName("아보카도 피자 조회")
	void test1() {
		Food food = foodRepository.findById(2L).orElseThrow(NullPointerException::new);

		System.out.println("food.getName() = " + food.getName());
		System.out.println("food.getPrice() = " + food.getPrice());

		System.out.println("아보카도 피자를 주문한 회원 정보 조회");
		System.out.println("food.getUser().getName() = " + food.getUser().getName());
	}

	@Test
	@Transactional
	@DisplayName("Robbie 고객 조회")
	void test2() {
		User user = userRepository.findByName("Robbie");
		System.out.println("user.getName() = " + user.getName());

		System.out.println("Robbie가 주문한 음식 이름 조회");
		for (Food food : user.getFoodList()) {
			System.out.println(food.getName());
		}
	}


	@Test
	@DisplayName("Robbie 고객 조회 실패")
	void test3() {
		User user = userRepository.findByName("Robbie");
		System.out.println("user.getName() = " + user.getName());

		System.out.println("Robbie가 주문한 음식 이름 조회");
		for (Food food : user.getFoodList()) {
			System.out.println(food.getName());
		}
	}

}

UserRepository

public interface UserRepository extends JpaRepository<User,Long> {
	User findByName(String name);
}

▶ test1() 실행 시

ㄴ “아보카도 피자”의 가격을 조회하려고 했을 뿐인데 자동으로 JOIN 문을 사용하여 연관관계가 설정되어있는 고객 테이블의 정보도 가져오고 있음

 

▶ test2() 실행 시

  • 이번에는 Robbie 고객을 조회한 후 Robbie 고객이 주문한 음식들의 이름을 조회했음
  • @OneToMany 즉, default가 지연 로딩으로 설정되어있기 때문에 우선 고객을 조회한 후
    user.getFoodList() 호출 즉, 주문한 음식의 정보가 필요한 시점에 음식 테이블에 해당 고객 Entity의 식별자 값을 사용하여 Select SQL이 수행되었음

 

  • 영속성 컨텍스트의 기능
    • 1차 캐시
    • 쓰기 지연 저장소
    • 변경 감지
    • 지연 로딩
      • 따라서 지연 로딩된 Entity의 정보를 조회하려고 할 때는 반드시 영속성 컨텍스트가 존재해야함!
      • ‘영속성 컨텍스트가 존재해야한다’라는 의미는 결국 ‘트랜잭션이 적용되어있어야 한다’라는 의미와 동일!

▶ test3() 실행 시

  • ‘Robbie 고객 조회 실패’ 테스트 코드를 확인해보시면 @Transactional이 test3() 메서드에 설정되어있지 않음
  • 즉, 트랜잭션이 적용되지 않았기 때문에 지연 로딩된 음식 Entity 정보들을 user.getFoodList() 즉, 필요한 시점에 조회하려고 하자 오류가 발생한 것임!
  • 따라서 지연 로딩된 정보를 조회하려고 할 때는 반드시 트랜잭션이 적용되어 영속성 컨텍스트가 존재하는지를 확인해야함!

 

 


음식 테이블과 고객 테이블이 N : 1 양방향 관계라 가정

 

entity>User

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

CascadeTest

@SpringBootTest
public class CascadeTest {

	@Autowired
	UserRepository userRepository;
	@Autowired
	FoodRepository foodRepository;


	@Test
	@DisplayName("Robbie 음식 주문")
	void test1() {
		// 고객 Robbie 가 후라이드 치킨과 양념 치킨을 주문합니다.
		User user = new User();
		user.setName("Robbie");

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

		user.addFoodList(food);

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

		user.addFoodList(food2);

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

}
  • Robbie가 음식을 주문하기 위해서는 위처럼 user, food, food2 모두 직접 save() 메서드를 호출하면서 영속화해야하는데
    JPA에서는 이를 간편하게 처리할 수 있는 방법으로 영속성 전이(CASCADE)의 PERSIST 옵션을 제공!!!
    • 영속성 전이 : 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황
    • 영속성 전이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정!

▼  ▼  ▼  ▼ 
entity>User

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

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    private List<Food> foodList = new ArrayList<>();

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

ㄴ 고객 Entity의 @OneToMany 애너테이션에 영속성 전이를 적용해서 음식 Entity도 자동으로 저장될 수 있도록 만들었음

CascadeTest

@Test
@DisplayName("영속성 전이 저장")
void test2() {
    // 고객 Robbie 가 후라이드 치킨과 양념 치킨을 주문합니다.
    User user = new User();
    user.setName("Robbie");

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

    user.addFoodList(food);

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

    user.addFoodList(food2);

    userRepository.save(user);
}

ㄴ CASCADE 설정을 적용했기 때문에 직접 음식 Entity 객체 food, food2를 영속 상태로 만들지 않아도 자동으로 저장

 

 

사용자가 주문 APP을 탈퇴하려고 하는데 주문한 음식 정보들까지 모두 삭제하도록 하려면?

entity>User

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

CascadeTest

@Test
@Transactional
@Rollback(value = false)
@DisplayName("Robbie 탈퇴")
void test3() {
    // 고객 Robbie 를 조회합니다.
    User user = userRepository.findByName("Robbie");
    System.out.println("user.getName() = " + user.getName());

    // Robbie 가 주문한 음식 조회
    for (Food food : user.getFoodList()) {
        System.out.println("food.getName() = " + food.getName());
    }

    // 주문한 음식 데이터 삭제
    foodRepository.deleteAll(user.getFoodList());

    // Robbie 탈퇴
    userRepository.delete(user);
}

 

ㄴ 주문한 음식 데이터를 삭제하기 위해서 지연 로딩된 음식 Entity들을 가져와 직접 삭제해준 후 Robbie 고객의 Entity를 삭제

  • JPA에서는 이를 간편하게 처리할 수 있는 방법으로 영속성 전이(CASCADE)의 REMOVE 옵션을 제공

▼  ▼  ▼  ▼  
entity>User

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

    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this);// 외래 키(연관 관계) 설정
    }
}
  • cascade = {CascadeType.PERSIST, CascadeType.REMOVE} 이렇게 중복으로 옵션 설정 가능
    ㄴ 고객 Entity의 @OneToMany 애너테이션에 연관된 음식 Entity도 자동으로 삭제될 수 있도록 REMOVE 옵션을 추가

CascadeTest

@Test
@Transactional
@Rollback(value = false)
@DisplayName("영속성 전이 삭제")
void test4() {
    // 고객 Robbie 를 조회합니다.
    User user = userRepository.findByName("Robbie");
    System.out.println("user.getName() = " + user.getName());

    // Robbie 가 주문한 음식 조회
    for (Food food : user.getFoodList()) {
        System.out.println("food.getName() = " + food.getName());
    }

    // Robbie 탈퇴
    userRepository.delete(user);
}

ㄴ Robbie 고객 Entity 객체를 조회한 후 해당 객체를 delete 하자 자동으로 연관된 음식 데이터들이 삭제됨!

 


  • CASCADE의 REMOVE 옵션을 적용하면 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제할 수 있었음
  • 하지만 REMOVE 옵션 같은 경우, 연관된 Entity와 관계를 제거했다고 해서 자동으로 해당 Entity가 삭제 되지는 않음!!!

OrphanTest

@Test
@Transactional
@Rollback(value = false)
@DisplayName("연관관계 제거")
void test1() {
    // 고객 Robbie 를 조회합니다.
    User user = userRepository.findByName("Robbie");
    System.out.println("user.getName() = " + user.getName());

    // 연관된 음식 Entity 제거 : 후라이드 치킨
    Food chicken = null;
    for (Food food : user.getFoodList()) {
        if(food.getName().equals("후라이드 치킨")) {
            chicken = food;
        }
    }
    if(chicken != null) {
        user.getFoodList().remove(chicken);
    }

    // 연관관계 제거 확인
    for (Food food : user.getFoodList()) {
        System.out.println("food.getName() = " + food.getName());
    }
}

ㄴ 후라이드 치킨 Entity 객체와 연관관계를 제거했지만 Delete SQL이 수행되지 않음 (테이블 보니 후라이드 치킨 안지워짐)

  • JPA에서는 이를 간편하게 처리할 수 있는 방법으로 orphanRemoval 옵션을 제공

▼  ▼  ▼  ▼  

entity>User

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

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this);// 외래 키(연관 관계) 설정
    }
}
  • Delete SQL이 수행되어 후라이드 치킨 데이터가 삭제된 것을 확인할 수 있음!
  • orphanRemoval 옵션도 REMOVE 옵션과 마찬가지로 해당 Entity 즉, Robbie Entity 객체를 삭제하면 연관된 음식 Entity들이 자동으로 삭제됨!

 

※ 주의할 점
orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야함

  • A와 B에 참조되고 있던 C를, B를 삭제하면서 같이 삭제하게 되면, A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생
  • 따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용불가!!!
    • ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않음
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.