주노 님의 블로그

20240822 본캠프 29일차 TIL 본문

TIL

20240822 본캠프 29일차 TIL

juno0432 2024. 8. 22. 23:23

본캠프 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을 사용한다

    그래서 연관관계의 주인은 어디임?
    연관관계의 주인은 테이블에 외래키가 있는곳으로 정한다.

    예시를 보자
    출처 : 스파르타코딩클럽 스프링 4주차 7강
    외래키가 있는 음식이 연관관계의 주인이 된다

    //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관계

    단방향
    출처 : 내일배움캠프 스프링 숙련 2주차 9강
    음식 하나가 여러명의 고객이 주문 할 수 있다.
    외래키의 주인은 음식이지만, 데이터베이스에서는 고객이 외래키를 가지고 있다.
    엔티티에서 외래키를 컨트롤하는건 음식 엔티티의 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한다.
    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=?
     insert는 3번씩 나간다
    또한 외래키를 저장하기위해 update도 두번 수행되는것을 알 수 있다.

    update를 날릴때 1번과 2번을 날리는것을 list에 넣어줬기때문에 확인 할 수 있다.
    테이블을 확인하면 정상적으로 삽입된 것을 확인 할 수 있다.

  • N:M관계
    출처 : 내일배움캠프 스프링 숙련 2주차 10강
    위 자료를 보면 음식과 고객은 N:M으로 이루어 질 수 있다.
    출처 : 내일배움캠프 스프링 숙련 2주차 10강

    이렇게 중간 테이블을 두어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시간 몰입했는가? 

어제 놀던만큼 풀 집중해서 공부했다..!

내일도 과제풀다가 주말까지는 과제 완성해보기로... 정진!