87일차 - JPA (N+1 문제, 다대다 관계, Query DSL)

Yohan·2024년 6월 27일
0

코딩기록

목록 보기
129/156
post-custom-banner

CascadeType

  • PERSIST : 부모가 갱신되면 자식도 같이 갱신
    • 부모의 리스트에 자식을 추가하거나 제거하면 데이터베이스에도 반영
  • REMOVE : 부모가 제거되면 자식도 같이 제거
    • ON DELETE CASCADE
  • ALL : 위의 내용을 전부 포함. 많은 기능이 내장
    // 1개(one)의 부서가, 여러 사원(many)의 정보를 가지고 있어서 @OneToMany
    //           상대방이 정의한 내 이름,   고아 삭제 ture,   연쇄삭제
    @OneToMany(mappedBy = "department", orphanRemoval = true,
            cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
    private List<Employee> employees = new ArrayList<>();

연관 관계의 주인

  • 일대다에서 다 쪽이 주인
  • 일 쪽에서 @OneToMany(mappedBy) 설정해야됨!

N+1 문제 및 Fetch Join

  • LAZY Loading의 문제가 존재
  • 부서와 사원 처럼 일대다 관계에서 각 부서마다 모든 사원을 select해야 하는 상황에서는 쿼리를 많이 보내게됨
    -> 이럴 때 Fetch Join을 사용하여 한 쿼리로 모든 데이터를 가져올 수 있음, 보통 LAZY Loading을 사용하다 필요에 의해 Fetch Join을 작성
  • JOIN FETCH는 순수한 SQL에는 없는 문법이다. 사실상 그냥 JOIN문
  • DepartmentRepository
    @Query("SELECT d FROM Department d JOIN FETCH d.employees")
    List<Department> getFetchEmployees();

다대다 관계

  • @ManyToMany 어노테이션을 사용하지 않고, 중간 엔터티클래스를 하나 만듦
  • Goods
package com.spring.jpastudy.chap05.entity;

import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Setter @Getter
@ToString(exclude = "purchaseList")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_mtm_goods")
public class Goods {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "goods_id")
    private Long id;

    @Column(name = "goods_name")
    private String name;

	// mappedBy: 반대편에서는 나를 뭐라고 부르는지
    @OneToMany(mappedBy = "goods", orphanRemoval = true, cascade = CascadeType.ALL)
    private List<Purchase> purchaseList = new ArrayList<>();
}
  • User
package com.spring.jpastudy.chap05.entity;

import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Setter
@Getter
@ToString(exclude = "purchaseList")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_mtm_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    @Column(name = "user_name")
    private String name;

	// 1명의 User는 여러개 구매가능 (OneToMany)
    @OneToMany(mappedBy = "user", orphanRemoval = true, cascade = CascadeType.ALL)
    private List<Purchase> purchaseList = new ArrayList<>();
}
  • Purchase
package com.spring.jpastudy.chap05.entity;

import lombok.*;

import javax.persistence.*;

@Setter
@Getter
@ToString(exclude = {"user", "goods"})
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_mtm_purchase")
public class Purchase {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "purchase_id")
    private Long id;

    // FK
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")  // 두 어노테이션 필수
    private User user; // 여기서 적은 변수명이 옆집의 mappedBy로 간다.

    // FK
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "goods_id")
    private Goods goods;

}

다대다 관계 시나리오 테스트

  • 각 repository에는 아무 메서드도 만들지 않은 상태
  • PurchaseRepositoryTest
    • EntityManager em; : 영속성 컨텍스트를 관리하는 객체
      -> 영속성 컨텍스트란 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
package com.spring.jpastudy.chap05.repository;

@SpringBootTest
@Transactional
@Rollback
class PurchaseRepositoryTest {

    @Autowired
    UserRepository userRepository;

    @Autowired
    GoodsRepository goodsRepository;

    @Autowired
    PurchaseRepository purchaseRepository;

    @Autowired
    EntityManager em; // 영속성 컨텍스트를 관리하는 객체
    // SELECT문을 날려주는 객체다

    private User user1;
    private User user2;
    private User user3;
    private Goods goods1;
    private Goods goods2;
    private Goods goods3;

    @BeforeEach
    void setUp() {
        user1 = User.builder().name("망곰이").build();
        user2 = User.builder().name("하츄핑").build();
        user3 = User.builder().name("쿠로미").build();
        goods1 = Goods.builder().name("뚜비모자").build();
        goods2 = Goods.builder().name("닭갈비").build();
        goods3 = Goods.builder().name("중식도").build();

        userRepository.save(user1);
        userRepository.save(user2);
        userRepository.save(user3);
        goodsRepository.save(goods1);
        goodsRepository.save(goods2);
        goodsRepository.save(goods3);
    }


    @Test
    @DisplayName("유저와 상품을 연결한 구매 기록 생성 테스트")
    void createPurchaseTest() {
        //given
        Purchase purchase = Purchase.builder()
                .user(user2) // user2가
                .goods(goods1) // goods1을 구매
                .build();
        //when
        purchaseRepository.save(purchase);

        // 영속성 컨텍스트를 초기화하면 SELECT문을 볼 수 있다.
        em.flush();
        em.clear();

        //then
        Purchase foundPurchase = purchaseRepository.findById(purchase.getId()).orElseThrow();

        System.out.println("\n\n\n구매한 회원정보: " + foundPurchase.getUser() + "\n\n");
        System.out.println("\n\n\n구매한 상품정보: " + foundPurchase.getGoods() + "\n\n");

        assertEquals(user2.getId(), foundPurchase.getUser().getId());
        assertEquals(goods1.getId(), foundPurchase.getGoods().getId());
    }


    @Test
    @DisplayName("특정 유저의 구매 목록을 조회한다.")
    void findPurchaseListTest() {
        //given 한 유저가 물건을 2개 구입
        Purchase purchase1 = Purchase.builder()
                .user(user1).goods(goods1).build();
        Purchase purchase2 = Purchase.builder()
                .user(user1).goods(goods3).build();

        //when 저장
        purchaseRepository.save(purchase1);
        purchaseRepository.save(purchase2);

        em.flush();
        em.clear();

        //then

        // 첫번째 회원의 id를 통해서 유저 정보 가져옴
        User user = userRepository.findById(user1.getId()).orElseThrow();
        // 유저 정보의 구입이력을 가지고 구입이력 리스트 꺼내옴
        List<Purchase> purchases = user.getPurchaseList();

        for (Purchase p : purchases) {
            System.out.printf("\n\n%s 님이 구매한 물품명 : %s\n\n",
                    user.getName(), p.getGoods().getName());
        }

        assertEquals(2, purchases.size());
        assertTrue(purchases.stream().anyMatch(p -> p.getGoods().equals(goods1)));
        assertTrue(purchases.stream().anyMatch(p -> p.getGoods().equals(goods3)));
    }


    @Test
    @DisplayName("특정 상품을 구매한 유저목록을 조회한다.")
    void findUserByGoods() {
        //given
        // goods1을 user2,3가 구매함.
        Purchase purchase1 = Purchase.builder()
                .user(user2).goods(goods1).build();
        Purchase purchase2 = Purchase.builder()
                .user(user3).goods(goods1).build();

        //when
        // 저장
        purchaseRepository.save(purchase1);
        purchaseRepository.save(purchase2);
        em.flush();
        em.clear();

        //then
        // goods1번의 정보를 가져옴
        Goods goods = goodsRepository.findById(goods1.getId()).orElseThrow();
        // 가져온 정보로 구매이력 리스트 꺼내옴
        List<Purchase> purchases = goods.getPurchaseList();

        for (Purchase p : purchases) {
            System.out.printf("\n\n%s 상품을 구매한 유저명 : %s\n\n",
                    goods.getName(), p.getUser().getName());
        }

        assertEquals(2, purchases.size());
        assertTrue(purchases.stream().anyMatch(p -> p.getUser().equals(user2)));
        assertTrue(purchases.stream().anyMatch(p -> p.getUser().equals(user3)));
    }


    @Test
    @DisplayName("구매기록 삭제 테스트")
    void deletePurchaseTest() {
        //given
        Purchase purchase = Purchase.builder()
                .user(user1).goods(goods1).build();
        Purchase savedPurchase = purchaseRepository.save(purchase);

        //when

        purchaseRepository.delete(savedPurchase);



        //then
        Purchase foundPurchase = purchaseRepository.findById(purchase.getId()).orElse(null);
        assertNull(foundPurchase);
    }


}


    @Test
    @DisplayName("회원이 탈퇴하면 구매기록이 삭제되어야 한다")
    void cascadeRemoveTest() {
        //given
        Purchase purchase1 = Purchase.builder()
                .user(user1).goods(goods2).build();

        Purchase purchase2 = Purchase.builder()
                .user(user1).goods(goods3).build();

        Purchase purchase3 = Purchase.builder()
                .user(user2).goods(goods1).build();

        purchaseRepository.save(purchase1);
        purchaseRepository.save(purchase2);
        purchaseRepository.save(purchase3);
        

        User user = userRepository.findById(user1.getId()).orElseThrow();
        List<Purchase> purchases = user.getPurchaseList();

        System.out.println("\n\nuser1's purchases = " + purchases + "\n\n");
        System.out.println("\n\nall of purchases = " + purchaseRepository.findAll() + "\n\n");

        userRepository.delete(user);

        //when

        List<Purchase> purchaseList = purchaseRepository.findAll();

        System.out.println("\n\nafter removing purchaseList = " + purchaseList + "\n\n");

        //then
        assertEquals(1, purchaseList.size());
    }

Query DSL

  • 기본 쿼리 지원은 편하지만 어쨌든 복잡한 쿼리를 사용해야 할때는 직접 만들어야 함
  • 문자열로 쿼리를 작성하게 되면, 오타가 나도 경고를 주지 않음
  • QueryDSL은 자바스타일로 쿼리문을 작성하여 컴파일에러를 일으키며 오타 검증 해줌.

JPQL과 Query DSL 차이점

  • JPQL은 문자열 기반 질의 언어입니다. SQL과 문법적으로 비슷하지만 객체 지향적입니다.
    예: SELECT e FROM Employee e WHERE e.name = :name
  • QueryDSL은 타입 세이프한 방식으로 쿼리를 작성할 수 있도록 도와줍니다. Java 코드로 쿼리를 작성하기 때문에 컴파일 시점에 문법 오류를 잡아낼 수 있습니다.

요약

  • JPQL은 JPA의 표준 쿼리 언어로, 문자열 기반이며 SQL과 유사하지만 객체 지향적입니다. 동적 쿼리 작성이 어렵고 컴파일 시점에 문법 오류를 잡아낼 수 없습니다.
  • QueryDSL은 타입 세이프한 쿼리 작성 도구로, Java 코드 기반으로 쿼리를 작성합니다. 동적 쿼리 작성이 용이하며, IDE 지원이 뛰어나고 컴파일 시점에 오류를 잡아낼 수 있습니다.
QEmployee employee = QEmployee.employee;
JPAQuery<?> query = new JPAQuery<>(entityManager);
List<Employee> employees = query.select(employee)
                                 .from(employee)
                                 .where(employee.name.eq(name))
                                 .fetch();

사용법

            idol.idolName.eq("리즈") // idolName = '리즈'
            idol.idolName.ne("리즈") // username != '리즈'
            idol.idolName.eq("리즈").not() // username != '리즈'
            idol.idolName.isNotNull() //이름이 is not null
            idol.age.in(10, 20) // age in (10,20)
            idol.age.notIn(10, 20) // age not in (10, 20)
            idol.age.between(10,30) //between 10, 30
            idol.age.goe(30) // age >= 30
            idol.age.gt(30) // age > 30
            idol.age.loe(30) // age <= 30
            idol.age.lt(30) // age < 30
            idol.idolName.like("_김%")  // like _김%
            idol.idolName.contains("김") // like %김%
            idol.idolName.startsWith("김") // like 김%
            idol.idolName.endsWith("김") // like %김

QueryDsl세팅

  • QueryDslConfig
package com.spring.jpastudy.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

// QueryDsl 세팅
@Configuration
public class QueryDslConfig {
    
    @PersistenceContext // 영속성 주입
    private EntityManager em;
    
    @Bean // 외부라이브러리를 스프링 컨테이너에 관리시키는 설정
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}
  • test
package com.spring.jpastudy.chap06_querydsl.repository;


import static com.spring.jpastudy.chap06_querydsl.entity.QIdol.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
//@Rollback(false)
class QueryDslBasicTest {

    @Autowired
    IdolRepository idolRepository;

    @Autowired
    GroupRepository groupRepository;

    // JPA의 CRUD를 제어하는 객체
    @Autowired
    EntityManager em;

    @Autowired
    JPAQueryFactory factory;

    @BeforeEach
    void setUp() {
        //given
        Group leSserafim = new Group("르세라핌");
        Group ive = new Group("아이브");

        groupRepository.save(leSserafim);
        groupRepository.save(ive);

        Idol idol1 = new Idol("김채원", 24, leSserafim);
        Idol idol2 = new Idol("사쿠라", 26, leSserafim);
        Idol idol3 = new Idol("가을", 22, ive);
        Idol idol4 = new Idol("리즈", 20, ive);

        idolRepository.save(idol1);
        idolRepository.save(idol2);
        idolRepository.save(idol3);
        idolRepository.save(idol4);

    }


  

    @Test
    @DisplayName("QueryDsl로 특정 이름의 아이돌 조회하기")
    void queryDslTest() {
        //given
        Idol foundIdol = factory
                //      *
                .select(idol)
                // Q타입.엔터티
                .from(idol) // 반드시 Q타입으로 적어야 함.
                .where(idol.idolName.eq("사쿠라"))
                .fetchOne();

        //then

        System.out.println("foundIdol = " + foundIdol);
        assertEquals("르세라핌", foundIdol.getGroup().getGroupName());
    }
}


    @Test
    @DisplayName("이름과 나이로 아이돌 조회하기")
    void searchTest() {
        //given
        String name = "리즈";
        int age = 20;
        //when
        Idol foundIdol = factory
                .select(idol)
                .from(idol)
                .where(
                        idol.idolName.eq(name)
                                .and(idol.age.eq(age))
                )
                .fetchOne();

        //then
        System.out.println("foundIdol = " + foundIdol);
        assertNotNull(foundIdol);
        assertEquals("아이브", foundIdol.getGroup().getGroupName());
    }



    @Test
    @DisplayName("조회 결과 반환하기")
    void fetchTest() {

        // 리스트 조회 (fetch)
        List<Idol> idolList = factory
                .select(idol)
                .from(idol)
                .fetch();

        // 단일행 조회 (fetchOne)
        Idol foundIdol = factory
                .select(idol)
                .from(idol)
                .where(idol.age.lt(21))
                .fetchOne();


        // 단일행 조회시 null safety를 위한 Optional로 받고 싶을 때
        Optional<Idol> foundIdolOptional = Optional.ofNullable(factory
                .select(idol)
                .from(idol)
                .where(idol.age.lt(21))
                .fetchOne());

        Idol foundIdol2 = foundIdolOptional.orElseThrow();



    }
profile
백엔드 개발자
post-custom-banner

0개의 댓글