테이블이 하나만 있을때는 괜찮지만 여러개가 있을때는 서로의 연관관계를 생각 해줘야한다. 예를들어 고객이 음식을 주문할때, 주문 정보는 어느 테이블에?
한명의 고객은 음식을 여러개 주문 가능하다 고객과 음식은 1:N 관계이다. (일대다)
하나의 음식은 여러명의 고객이 시켜먹을수 있다. 음식과 고객은 N:1 관계이다. (다대일)
이렇게 food에는 user_id를 가지고있고 user은 food_id를 가지고있다. 불필요한 이름과 정보가 중복되는 문제가 있다. 위 처럼 음식은 음식에 대한 정보만, 고객은 고객에 대한 정보만 가지고 주문 테이블을 만들어 그 두개를 매핑만 하면된다.
고객 한명은 음식 N개를 주문할 수 있다 음식 1개는 고객 N명에게 주문 될 수 있다
결론적으로 고객과 음식은 N:M관계이다
N:M관계의 연관 관계를 해결하기위해 주문 테이블처럼 중간 테이블을 만들 수 있다.
테이블의 방향 단방향은 유저 테이블에서만 음식을 참조할수 있다. 반대는 X 양방향은 서로를 참조할수있을때를 말한다. DB는 테이블간의 방향이 있을까?
SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM users u
INNER JOIN orders o on u.id = o.user_id
INNER JOIN food f on o.food_id = f.id
WHERE o.user_id = 1;
SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM food f
INNER JOIN orders o on f.id = o.food_id
INNER JOIN users u on o.user_id = u.id
WHERE o.user_id = 1;
user 테이블과 food테이블 기준으로 조회하였을때 결과는 똑같다
rdb는 외래키만 가지고 있다면 join으로 서로를 찾을 수 있으므로 사실상 양방향이다.
그럼 이 관계를 표현할 수 없는 자바에서는 어떻게 표현할까?
객체는 기본적으로 참조를 통한 연관관계를 맺는다 하지만 이 연관관계는 언제나 단방향이다
객체를 양방향으로 만들고 싶다면 반대쪽에서도 필드를 추가해 서로를 참조해야한다.
jpa에서 연관관계를 명시할수있는 어노테이션을 제공해준다
양방향 관계
@ManyToOne@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
두개의 entity에서 서로를 참조하는것을 볼 수 있다. @ManyToOne, @OneToMany 등등을 사용한다. 실제로 List 타입으로 저장된건 아니다!
단방향 관계
@ManyToOne@JoinColumn(name = "user_id")
private User user;
음식 entity에서만 user entity를 참조할 수 있다.
entity에서는 상대 entity를 참조하고 있지않다면 상대 entity를 조회할 방법이 없다. 따라서 자바 entity에서는 db와달리 방향의 개념이 있다.
일대일 관계
@OneToOen으로 1:1 관계를 맺어준다
외래키의 주인(연관관계의 주인) 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야한다. 연관관계의 주인만이 외래키를 관리(저장/수정/삭제)할 수 있다. 주인이 아닌쪽은 읽기만 할 수 있다
둘중 누가 외래키를 관리할것인가? mappedBy속성을 설정하는것이다. 주인이 아니면 mappedBy 속성을 사용하여, 속성의 값으로 연관관계의 주인을 지정(필드명을 지정)해준다 mappedby가 설정된 곳은 외래키를 직접 관리하지 않으며 읽기 전용으로 취급된다는 점이 있다. 주인은 mappedBy를 사용하지 않고 joincolumn을 사용한다
위 list는 데이터베이스에 적용되는게 아니라. 그냥 자바단에서 조회하기위한 수단임을 이해해야한다.
food 엔티티는 user엔티티와 many-to-one 관계를 가지며 이를 위해 @ManyToOne어노테이션과 @JoinColumn어노테이션을 사용하고 user 엔티티는 food 엔티티와 one-to-many 관계를 가지며 @oneToMany 어노테이션과 mappedby 속성을 사용하여 관계를 설정한다 @OneToMany(mappedBy ="join to column의 상대 엔티티의 필드 명") 을 사용하면 된다.
@Test@Rollback(value = false)
@DisplayName("N대1 양방향 테스트")
voidtest4() {
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);
}
위 코드로 양방향 관계를 테스트하는 코드가 작성되었다 user과 food 객체를 생성하고 주인인 food 객체에 user객체를 할당하여 외래키를 설정하는 모습을 볼 수 있다.
위 결과를 보면 many to one이 적용된것을 볼 수 있다. 여러개의 음식이 하나의 고객을 받고있는것을 볼 수 있다.
그렇다면 외래키의 주인이 아닌곳에서는 등록이 불가능한걸까? 약간의 편법? 을 사용하면 가능하다
@Test@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
voidtest3() {
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);
}
외래키의 주인이 아닌 user 엔티티에서도 food 객체를 추가 할 수 있는데 이를 위해 user 엔티티에 addFoodList메서드를 정의하여 Food 리스트에 추가하고 결국 food.setUser을 하는거니 거의 비슷하게 동작한다고 볼 수 있다.
조회
@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에서 List로 food를 설정한것을 알 수 있다. user에 해당하는 list를 뽑아온다.
1대N관계
단방향 출처 : 내일배움캠프 스프링 숙련 2주차 9강음식 하나가 여러명의 고객이 주문 할 수 있다. 외래키의 주인은 음식이지만, 데이터베이스에서는 고객이 외래키를 가지고 있다. 엔티티에서 외래키를 컨트롤하는건 음식 엔티티의 LIST이다. LIST를 통해 외래키를 관리해줘야한다.
FOOD가 1의 관계다보니 ONETOMANY어노테이션으로 설정한다. 기존의 joinColumn은 user_id였고 User user이었는데 그동안은 실제 데이터베이스에서 외래키를 갖는쪽은 다측인 food였지만 현재는 다의 관계가 user이기때문에 외래키를 user에 놓을 수 밖에없다.
요약하자면 외래키의 주인은 food 이지만, 실제 테이블에 저장되는 외래키는 user이기때문에 상대 엔티티의 외래키를 저장해놓는것.
@Entity@Table(name = "users")
public class User {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
privateStringname;
}
기존의 방법은 user.set~~()를 하면 됐었지만, 외래키의 주인을 통해서만 외래키를 조회해야한다 insert문이 날라간다음, user 테이블로가서 update를 추가로 실행된다. 외래키를 직접 관리하는 주체가 USER에 있기때문에 하이버네이트가 기본적으로 연관된 엔티티를 삽입 한 후, 외래키를 설정하는 추가 UPDATE쿼리가 나가기 때문이다.
food에 변화를 주었는데 update는 user 테이블을 대상으로 수행한다 > 추척에 힘듬
양방향 OneToMany는 mappedBy옵션을 지원하지 않는다.
@Entity@Table(name = "users")
public class User {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
privateStringname;
@ManyToOne
@JoinColumn(name = "food_id", insertable = false, updatable = false)
privateFoodfood;
}
굳이 굳이 한다면 ManyToOne을 작성한다 임시방편으로 다대일쪽은 외래키를 읽기만 가능한 insertable과 updateble을 false로 설정한다
@Test@Rollback(value = false)
@DisplayName("1대N 단방향 테스트")
voidtest1() {
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 쿼리 발생을 확인할 수 있습니다.
}
위코드를 테스트 해볼예정이다. user객체를 두개 만들고 food 객체를 만든다.
그리고 user은 직접 set을 할 수 없으니 list에서 add한다
그리고 각각을 save한다.
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.User */insertinto
users (name)
values
(?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.User */insertinto
users (name)
values
(?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food */insertinto
food (name, price)
values
(?, ?)
Hibernate:
update
users
set
food_id=?
where
id=?
Hibernate:
update
users
set
food_id=?
where
id=?
insert는 3번씩 나간다 또한 외래키를 저장하기위해 update도 두번 수행되는것을 알 수 있다.
update를 날릴때 1번과 2번을 날리는것을 list에 넣어줬기때문에 확인 할 수 있다. 테이블을 확인하면 정상적으로 삽입된 것을 확인 할 수 있다.
N:M관계 출처 : 내일배움캠프 스프링 숙련 2주차 10강위 자료를 보면 음식과 고객은 N:M으로 이루어 질 수 있다. 출처 : 내일배움캠프 스프링 숙련 2주차 10강 이렇게 중간 테이블을 두어N:M관계를 풀어내어 작성한다.
중간 테이블을 위 처럼 직접 생성하여 관리하면, 변경 발생시 컨트롤하기 쉽기 때문에 확장성에 좋다. FOOD와 USER은 각각 ORDER를 참조하는 MAPPEDBY를 설정하였다 외래키의 주인은 ORDER로 두 객체를 관리한다.
@Test@Rollback(value = false)
@DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용")
voidtest5() {
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());
}
}
위 코드로 정화갛게 등록되는지 테스트해본다. 먼저 user객체를 두개 만들고 아보카도 피자는 user1, 2에 등록 고구마피자는 user1에만 등록한뒤 각각의 레파지토리에 저장한다.
-------------userRepository.save(user, user2)에 대한 insert ------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.User */insertinto
users (name)
values
(?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.User */insertinto
users (name)
values
(?)
-------------foodRepository.save(food, food2)에 대한 insert ------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food */insertinto
food (name, price)
values
(?, ?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food */insertinto
food (name, price)
values
(?, ?)
-------------이미 영속 상태의 객체를 참조했기때문에 따로 select문이 나가지않음 ------
user.getName() = Robbie
f.getName() = 아보카도 피자
f.getPrice() =50000.0
f.getName() = 고구마 피자
f.getPrice() =30000.0-------------food와 user간의 중간테이블에 관한 insert food,2.addUserList(user) ------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food.userList */insertinto
orders (food_id, user_id)
values
(?, ?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food.userList */insertinto
orders (food_id, user_id)
values
(?, ?)
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food.userList */insertinto
orders (food_id, user_id)
values
(?, ?)
위 설명에 관한 주석을 달아놨다
@Test@Rollback(value = false)
@DisplayName("중간 테이블 Order Entity 테스트")
voidtest1() {
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);
}
중간 테이블을 이용하여 위 생성 테스트를 해보자
코드는 user객체 생성 food 객체 생성 주문 저장 및 연관관계설정 각각 저장 순이다
결과는 아래 세개의 save가 날라갈것이다.
--------테이블 생성 쿼리----------
Hibernate:
createtable food (
id bigintnotnull auto_increment,
name varchar(255),
price float(53) notnull,
primary key (id)
) engine=InnoDB
Hibernate:
createtable orders (
id bigintnotnull auto_increment,
food_id bigint,
user_id bigint,
primary key (id)
) engine=InnoDB
Hibernate:
createtable users (
id bigintnotnull auto_increment,
name varchar(255),
primary key (id)
) engine=InnoDB
--------외래키 설정 쿼리 쿼리----------
Hibernate:
altertable orders
addconstraint FK5g4j2r53ncoltplogbnqlpt30
foreign key (food_id)
references food (id)
Hibernate:
altertable orders
addconstraint FK32ql8ubntj5uh44ph9659tiih
foreign key (user_id)
references users (id)
2024-08-23T17:31:01.720+09:00 INFO 36448--- [jpa-advance] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'2024-08-23T17:31:01.805+09:00 WARN 36448--- [jpa-advance] [ Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning2024-08-23T17:31:02.209+09:00 INFO 36448--- [jpa-advance] [ Test worker] c.sparta.jpaadvance.relation.OrderTest : Started OrderTest in 2.062 seconds (process running for 2.677)--------user 객체 삽입 쿼리----------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.User */insertinto
users (name)
values
(?)
--------food 생성 쿼리----------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Food */insertinto
food (name, price)
values
(?, ?)
--------orders 생성 쿼리----------
Hibernate:
/* insert for
com.sparta.jpaadvance.entity.Order */insertinto
orders (food_id, user_id)
values
(?, ?)
orders의 food_id와 user_id는 각각 위에서 자동 생성된 id값을 넣어줄것이다.
지연로딩과 즉시로딩
즉시 로딩 연관된 엔티티를 함께 조회하여 메모리에 로드하는 방식 (엔티티를 조회할때 연관된 엔티티 모두 조회)
지연 로딩 실제로 연관된 엔티티에 접근할때까지 데이터를 로드하지 않다가, 필요할때 쿼리를 날리는 방식
지연로딩과 즉시로딩을 테스트 하기 위해서는 연관관계를 바꿔보자 주문 : 아니 왜 나 왕따시켜요
food는 다대일 관계의 다측이므로 외래키를 소유하고있어서 manyToOne 어노테이션과 joincolumn어노테이션을 만들고 user의 일대다 측에서는 @OneToMany에서는 mappedby를 추가해줘야한다 위 설정으로인해 food엔티티가 user 엔티티를 참조한다.
위 테스트코드를 한번 살펴보자 food를 찾아서 이름과 가격을 출력한뒤 그것을 주문한 회원 정보를 조회하는 코드인것을 볼 수 있다.
Hibernate:select
f1_0.id,
f1_0.name,
f1_0.price,
u1_0.id,
u1_0.name
from
food f1_0
left join
users u1_0
on u1_0.id=f1_0.user_id
where
f1_0.id=?
food.getName() = 아보카도 피자
food.getPrice() = 50000.0
아보카도 피자를 주문한 회원 정보 조회
food.getUser().getName() = Robbie
똑같이 user를 조회해도 쿼리문이 한번밖에 실행되지 않았다..
그럼 미리미리 조인을 해서 들고온다는것인데 왜일까?
ManyToOne 어노테이션의 fetchType는 기본이 eager이고 (select문을 던지면서 다른것도 모두 가져옴) OneToMany 어노테이션의 fetchType는 기본이 Lazy이다. (select문을 던지면서 지금 필요한 객체만 들고옴)
일단 아래의 코드를 보자
@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());
}
}
위 코드를 보면 먼저 findByName으로 user를 찾는다. 그리고 user에 해당하는 food를 찾아온다.
Hibernate:
/* <criteria> */ select
u1_0.id,
u1_0.name
from
users u1_0
where
u1_0.name=?
user.getName() = Robbie
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=?
고구마 피자
아보카도 피자
후라이드 치킨
첫번째 select문은 findByName으로 인해 user를 찾는 select이다
그리고 두번째 select문은 food테이블에서 유저에 해당하는 음식을 찾는다 이미 위 하이버네이트에 의해 찾은 userid를 where절에서 사용할 수 있는것이다. one쪽인 user은 lazy기 때문에 food를 뒤늦게 찾는것이다. 그래서 조인문이 나가지 않는 것을 확인 할 수 있다.
Hibernate:
/* <criteria> */select
u1_0.id,
u1_0.name
from
users u1_0
where
u1_0.name=?
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=?
food.getName() = 고구마 피자
food.getName() = 아보카도 피자
Hibernate:
/* delete for com.sparta.jpaadvance.entity.Food */deletefrom
food
where
id=?
하이버네이트가 자동으로 delete를 보내는것을 알 수 있다. 근데 CasCadeType.REMOVE를 하지 않았는데도 적용이되는것을 볼 수 있다. orphanRemove는CasCadeType.REMOVE도 지원하는것을 볼 수 있다.
참고로 orphanRemoval이나 remove옵션을 사용할때 삭제하려고 하는 연관된 엔티티를 다른곳에서 참조하고 있는건 아닌지 확인해야한다
예를 들면 user과 food 엔티티가 있고 user가 여러 food 엔티티를 소유하고 있다고 가정해보자 user가 food의 엔티티와 관계를 끊으면서 orphanRemoval 옵션을 사용하여 해당 food를 삭제하려고하는데. food 엔티티를 order에서 참조하고 있다면?..
user가 food를 삭제했지만 order은 여전히 food를 참조하고 있다 >> 데이터 무결성 위반 오류