Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- replaceAll()
- 자바 스트링
- 스프링환경설정
- 베주계수
- ineer join
- string
- 최소공배수
- 최대공약수
- Git사용법
- StringBuilder
- string과 stringbuilder
- git 컨벤션
- addDoc
- while과 two-pointer
- 래퍼타입
- toLowerCase()
- 유클리드호제법
- sql 데이터형 변환
- islowercase()
- 스프링뼈대
- stringbuilder의 reverse()
- 자바 최소공배수
- 프로그래머스 레벨1
- 자바 최대공약수
- 모던자바
- isuppercase()
- 동일성과 동등성
- 최대공약수와 최소공배수
- 자바 유클리드
- 스프링
Archives
- Today
- Total
주노 님의 블로그
20240822 본캠프 29일차 TIL 본문
본캠프 29일차 내용 간단요약
- 09:00 ~ 10:00 : 코드카타
- 10:00 ~ 10:10 : 팀 회의
- 10:10 ~ 12:00 : 강의
2주차 6강 ~ 2주차 7강 - 12:00 ~ 13:00 : 점심시간
- 13:00 ~ 16:30 : 강의
2주차 8강 ~ 2주차 13강 - 16:30 ~ 17:30 : 면담
- 18:00 ~ 19:00 : 저녁시간
- 19:00 ~ 21:00 : 복습
- 19:40 ~ 20:10 : 면담
휴~ 휴~ 번아웃 올뻔!
오늘 해야할 일 ✔️ 🔺 ❌
✔️ 강의 2-13까지 듣기
jpa
더보기
연관관계
- 테이블이 하나만 있을때는 괜찮지만 여러개가 있을때는 서로의 연관관계를 생각 해줘야한다.
예를들어 고객이 음식을 주문할때, 주문 정보는 어느 테이블에?
한명의 고객은 음식을 여러개 주문 가능하다
고객과 음식은 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을 사용한다
그래서 연관관계의 주인은 어디임?
연관관계의 주인은 테이블에 외래키가 있는곳으로 정한다.
예시를 보자
외래키가 있는 음식이 연관관계의 주인이 된다
//food 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; } //user entity @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToOne(mappedBy = "user") private Food food; }
연관관계의 주인인 food에서 @OneToOne 어노테이션을 부착하고
JoinColumn으로 연결될 컬럼을 선택해준다
그리고 연관될 객체를 필드로 가져온다
만약 joincolumn 에너테이션이 없다면 의도하지 않은 중간테이블이 생길 수 있다.
그리고 user쪽에서는 @OneToOne 어노테이션을 사용하며
mappedby의 속성값은 외래키의 주인의 필드명을 뜻한다.
쉽게말해서 저거라는뜻
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 "다"쪽이 외래키를 가진다
@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); }
양방향 테스트는 위와 같이 하면된다.
새로운 비영속 user객체와 food 객체를 생성한후
food 객체에 user 객체를 설정하는 것을 볼 수 있다.
이 이유는 food entity가 외래키를 가지고 있기때문에 연관관계의 주인이 food에게 있다는 뜻이다. - N대1 관계
@ManyToOne으로 설정한다.
@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<>(); }
위 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 양방향 테스트") 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); }
위 코드로 양방향 관계를 테스트하는 코드가 작성되었다
user과 food 객체를 생성하고 주인인 food 객체에 user객체를 할당하여 외래키를 설정하는 모습을 볼 수 있다.
위 결과를 보면 many to one이 적용된것을 볼 수 있다.
여러개의 음식이 하나의 고객을 받고있는것을 볼 수 있다.
그렇다면 외래키의 주인이 아닌곳에서는 등록이 불가능한걸까?
약간의 편법? 을 사용하면 가능하다
@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); }
userentity에 addFoodList를 정의해놨다.
public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this); // 외래 키(연관 관계) 설정 }
외래키의 주인이 아닌 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관계
단방향
음식 하나가 여러명의 고객이 주문 할 수 있다.
외래키의 주인은 음식이지만, 데이터베이스에서는 고객이 외래키를 가지고 있다.
엔티티에서 외래키를 컨트롤하는건 음식 엔티티의 LIST이다.
LIST를 통해 외래키를 관리해줘야한다.
@Entity @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @OneToMany @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼 private List<User> userList = new ArrayList<>(); }
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; private String name; }
기존의 방법은 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; private String name; @ManyToOne @JoinColumn(name = "food_id", insertable = false, updatable = false) private Food food; }
굳이 굳이 한다면 ManyToOne을 작성한다
임시방편으로 다대일쪽은 외래키를 읽기만 가능한 insertable과 updateble을 false로 설정한다
일대다 양방행 매핑이라기 보다는 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가해서 일대다 양방향처럼 보이도록 하는것이다.
따라서 일대다 단방향 매핑이 가지는 단점을 그대로 가진다.
@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 쿼리 발생을 확인할 수 있습니다. }
위코드를 테스트 해볼예정이다.
user객체를 두개 만들고 food 객체를 만든다.
그리고 user은 직접 set을 할 수 없으니 list에서 add한다
그리고 각각을 save한다.
insert는 3번씩 나간다Hibernate: /* insert for com.sparta.jpaadvance.entity.User */insert into users (name) values (?) Hibernate: /* insert for com.sparta.jpaadvance.entity.User */insert into users (name) values (?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into food (name, price) values (?, ?) Hibernate: update users set food_id=? where id=? Hibernate: update users set food_id=? where id=?
또한 외래키를 저장하기위해 update도 두번 수행되는것을 알 수 있다.
update를 날릴때 1번과 2번을 날리는것을 list에 넣어줬기때문에 확인 할 수 있다.
테이블을 확인하면 정상적으로 삽입된 것을 확인 할 수 있다. - N:M관계
위 자료를 보면 음식과 고객은 N:M으로 이루어 질 수 있다.
이렇게 중간 테이블을 두어N:M관계를 풀어내어 작성한다.
@Entity @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 @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 @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; }
중간 테이블을 위 처럼 직접 생성하여 관리하면, 변경 발생시 컨트롤하기 쉽기 때문에 확장성에 좋다.
FOOD와 USER은 각각 ORDER를 참조하는 MAPPEDBY를 설정하였다
외래키의 주인은 ORDER로 두 객체를 관리한다.
@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()); } }
위 코드로 정화갛게 등록되는지 테스트해본다.
먼저 user객체를 두개 만들고
아보카도 피자는 user1, 2에 등록
고구마피자는 user1에만 등록한뒤
각각의 레파지토리에 저장한다.
-------------userRepository.save(user, user2)에 대한 insert ------ Hibernate: /* insert for com.sparta.jpaadvance.entity.User */insert into users (name) values (?) Hibernate: /* insert for com.sparta.jpaadvance.entity.User */insert into users (name) values (?) -------------foodRepository.save(food, food2)에 대한 insert ------ Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into food (name, price) values (?, ?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into 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 */insert into orders (food_id, user_id) values (?, ?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food.userList */insert into orders (food_id, user_id) values (?, ?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food.userList */insert into orders (food_id, user_id) values (?, ?)
위 설명에 관한 주석을 달아놨다
@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); }
중간 테이블을 이용하여 위 생성 테스트를 해보자
코드는
user객체 생성
food 객체 생성
주문 저장 및 연관관계설정
각각 저장 순이다
결과는 아래 세개의 save가 날라갈것이다.
--------테이블 생성 쿼리---------- Hibernate: create table food ( id bigint not null auto_increment, name varchar(255), price float(53) not null, primary key (id) ) engine=InnoDB Hibernate: create table orders ( id bigint not null auto_increment, food_id bigint, user_id bigint, primary key (id) ) engine=InnoDB Hibernate: create table users ( id bigint not null auto_increment, name varchar(255), primary key (id) ) engine=InnoDB --------외래키 설정 쿼리 쿼리---------- Hibernate: alter table orders add constraint FK5g4j2r53ncoltplogbnqlpt30 foreign key (food_id) references food (id) Hibernate: alter table orders add constraint 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 warning 2024-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 */insert into users (name) values (?) --------food 생성 쿼리---------- Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into food (name, price) values (?, ?) --------orders 생성 쿼리---------- Hibernate: /* insert for com.sparta.jpaadvance.entity.Order */insert into orders (food_id, user_id) values (?, ?)
orders의 food_id와 user_id는 각각 위에서 자동 생성된 id값을 넣어줄것이다.
지연로딩과 즉시로딩
- 즉시 로딩
연관된 엔티티를 함께 조회하여 메모리에 로드하는 방식 (엔티티를 조회할때 연관된 엔티티 모두 조회)
지연 로딩
실제로 연관된 엔티티에 접근할때까지 데이터를 로드하지 않다가, 필요할때 쿼리를 날리는 방식
지연로딩과 즉시로딩을 테스트 하기 위해서는 연관관계를 바꿔보자
주문 : 아니 왜 나 왕따시켜요
음식과 고객을 n:1관계로 변환해보자
@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; } @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<>(); }
food는 다대일 관계의 다측이므로 외래키를 소유하고있어서
manyToOne 어노테이션과 joincolumn어노테이션을 만들고
user의 일대다 측에서는
@OneToMany에서는 mappedby를 추가해줘야한다
위 설정으로인해 food엔티티가 user 엔티티를 참조한다.
@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()); }
위 테스트코드를 한번 살펴보자
food를 찾아서 이름과 가격을 출력한다.
자세히보면 food id뿐만아니라 user_id까지 join을 한것을 볼 수 있다.
그럼 아래는 어떨까?
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()); }
위 테스트코드를 한번 살펴보자
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를 뒤늦게 찾는것이다. 그래서 조인문이 나가지 않는 것을 확인 할 수 있다.
@Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; }
다측인 food도 물론 lazy로 수정 가능하다
어노테이션 옆에 (fetch = FetchType.LAZY)로 하면 된다
아까의 아보카도 피자 조회를 다시 테스트해보자
대충 엄청난 오류가 뜬다.
지연로딩은 필요한 시점까지 데이터베이스에 던지는 쿼리를 미루는 방식이므로, 트랜잭션이 유지되고 있어야 엔티티에 접근이 가능함
@transaction이 적용되어야한다.
@Test @Transactional @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()); }
위처럼 트랜잭션을 달아주자
Hibernate: select f1_0.id, f1_0.name, f1_0.price, f1_0.user_id from food f1_0 where f1_0.id=? food.getName() = 아보카도 피자 food.getPrice() = 50000.0 아보카도 피자를 주문한 회원 정보 조회 Hibernate: select u1_0.id, u1_0.name from users u1_0 where u1_0.id=? food.getUser().getName() = Robbie
지연로딩으로 변경하니, join문이 사라지고, 회원 정보를 조회하려고 할때 하이버네이트가 쿼리를 던지는 것을 볼 수 있다.
영속성 전이
- 영속성 전이
부모 엔티티가 특정 작업을 수행할 때 그 작업이 연관된 자식 엔티티에도 자동으로 전이되는것! - 영속성 전이를 이용한 추가
아래 코드를 보자
@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); }
user객체를 정의한후
food 객체 2개를 넣고 각각 리스트에도 넣는다
그리고 각각 객체를 save한다.
세개를 모두 수동으로 영속을 해야하는데
저렇게 귀찮은 방법 말고 다른방법이 없을까?
jpa는 간편하게 처리할 수 있는 방법으로 영속성 전이의 persist 옵션을 제공한다.
@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) { foodList.add(food); food.setUser(this); } }
user entity의 foodlist가 같이 전파가되어서 한번에 저장이 되었으면좋겠다..
라서 foodList가 영속성 전이가 되면 된다.
casecad 옵션으로 persist를 주게되면 영속성 전이가 된다.
@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); }
그리고 위 코드로 테스트를 해보자
casecadeType.PERSIST를 적용한 후의 상황이다
USER엔티티만 SAVE를 해도 연관된 FOOD 엔티티들이 자동으로 저장되는 것을 볼 수 있다.
한줄의 코드만으로 모든 데이터가 영속화되는 장점이 있다.
Hibernate: /* insert for com.sparta.jpaadvance.entity.User */insert into users (name) values (?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into food (name, price, user_id) values (?, ?, ?) Hibernate: /* insert for com.sparta.jpaadvance.entity.Food */insert into food (name, price, user_id) values (?, ?, ?)
- 영속성 전이를 이용한 삭제
@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); }
user를 삭제하는 과정이다
먼저 user를 찾은다음 robbie와 연관된 음식을 삭제한뒤
user를 삭제한다.
여기서 user와 연관된 food 엔티티들이 one to many 방식의 lazy로딩이기때문에 이를 삭제하려면 트랜잭션이 필요하다
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 */delete from food where id=? Hibernate: /* delete for com.sparta.jpaadvance.entity.Food */delete from food where id=? Hibernate: /* delete for com.sparta.jpaadvance.entity.User */delete from users where id=?
일단 select문으로 유저를 찾아오고
다음 select문에서 user와 연관된 음식을 찾아온뒤
음식을 차례로 delete를 하고
마지막으로 user를 삭제한다.
현재는 delete 를 두번이나 하고있다!
위 코드도 쉽게 바꾸는 방법이 없을까?
@OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) private List<Food> foodList = new ArrayList<>(); public void addFoodList(Food food) { foodList.add(food); food.setUser(this); }
casecade 옵션에 CascadeType.REMOVE옵션이 추가되었다.
@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); }
이제 USER엔티티를 삭제할때 연관된 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 */delete from food where id=? Hibernate: /* delete for com.sparta.jpaadvance.entity.Food */delete from food where id=? Hibernate: /* delete for com.sparta.jpaadvance.entity.User */delete from users where id=?
하이버네이트는 아까와 같은 응답을 던진것을 볼 수있다.
이렇듯 영속성 전이를 이용하여 한번에 엔티티를 추가, 삭제를 하는 방법을 배웠다.
고아 entity 삭제
- 고아엔티티
JPA에서 부모 엔티티와 연관관계가 끊어진 자식엔티티를 말한다.
더이상 부모 엔티티에 의해 참조가 되지 않으므로 사용될 일이 없다
@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()); } }
위 코드는 엔티티간의 연관관계를 제거하고 있다.
robbie인 user 엔티티를 조회한다.
food를 순회하며 후라이드 치킨일 경우, 해당 list에서 remove메서드를 이용하여 연관관계를 끊고있다.
단지, 연관관계를 제거한것 뿐이지 삭제는 아니다.
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() = 아보카도 피자
실제로 하이버네이트가 날린 쿼리를 보면 그냥 select문으로 끝난다.
jpa는 연관관계를 끊는것 만으로도 자동으로 삭제를 시켜주는 옵션이있다
orphanRemoval =ture로 하면
위 객체와 연관된 쿼리는 관계가 끊기는 즉시 삭제가된다.
@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) private List<Food> foodList = new ArrayList<>();
위 테스트코드를 다시 실행해보자.
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 */delete from 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를 참조하고 있다 >> 데이터 무결성 위반 오류
그래서 위 두가지옵션을 사용할때는 조심 또 조심해야한다.!!
즉석에서 진행된 매니저님들과의 인터뷰
뭔가뭔가 귀여운거나 신기한 플러그인을 많이 알아가고
내배캠을 진행하면서 생길 일들 꿀팁에 대해서 알아왔다
자세한 내용은 비공개 ㅇㅅㅇ
바로 시작된 튜터님과의 인터뷰
예의를 갖추기 위해 똑같은 의상으로 차려입고 시작한 인터뷰
20분 정도의 인터뷰에 더 더 많은걸 알려주시려고 하시는 튜터님께 감사했다!
오늘의 회고 & 12시간 몰입했는가?
어제 놀던만큼 풀 집중해서 공부했다..!
내일도 과제풀다가 주말까지는 과제 완성해보기로... 정진!
'TIL' 카테고리의 다른 글
20240825 주말에도 나와버린 나의 과제 작성기 (0) | 2024.08.26 |
---|---|
20240823 본캠프 30일차 TIL (0) | 2024.08.23 |
20240821 본캠프 28일차 TIL (0) | 2024.08.21 |
20240819 본캠프 26일차 TIL (0) | 2024.08.19 |
20240817 주말에도 나와버린 나 TIL (0) | 2024.08.17 |