CascadeType
- PERSIST : 부모가 갱신되면 자식도 같이 갱신
- 부모의 리스트에 자식을 추가하거나 제거하면 데이터베이스에도 반영
- REMOVE : 부모가 제거되면 자식도 같이 제거
- ALL : 위의 내용을 전부 포함. 많은 기능이 내장
@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;
@OneToMany(mappedBy = "goods", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Purchase> purchaseList = new ArrayList<>();
}
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;
@OneToMany(mappedBy = "user", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Purchase> purchaseList = new ArrayList<>();
}
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;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@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;
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() {
Purchase purchase = Purchase.builder()
.user(user2)
.goods(goods1)
.build();
purchaseRepository.save(purchase);
em.flush();
em.clear();
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() {
Purchase purchase1 = Purchase.builder()
.user(user1).goods(goods1).build();
Purchase purchase2 = Purchase.builder()
.user(user1).goods(goods3).build();
purchaseRepository.save(purchase1);
purchaseRepository.save(purchase2);
em.flush();
em.clear();
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() {
Purchase purchase1 = Purchase.builder()
.user(user2).goods(goods1).build();
Purchase purchase2 = Purchase.builder()
.user(user3).goods(goods1).build();
purchaseRepository.save(purchase1);
purchaseRepository.save(purchase2);
em.flush();
em.clear();
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() {
Purchase purchase = Purchase.builder()
.user(user1).goods(goods1).build();
Purchase savedPurchase = purchaseRepository.save(purchase);
purchaseRepository.delete(savedPurchase);
Purchase foundPurchase = purchaseRepository.findById(purchase.getId()).orElse(null);
assertNull(foundPurchase);
}
}
@Test
@DisplayName("회원이 탈퇴하면 구매기록이 삭제되어야 한다")
void cascadeRemoveTest() {
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);
List<Purchase> purchaseList = purchaseRepository.findAll();
System.out.println("\n\nafter removing purchaseList = " + purchaseList + "\n\n");
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("리즈")
idol.idolName.ne("리즈")
idol.idolName.eq("리즈").not()
idol.idolName.isNotNull()
idol.age.in(10, 20)
idol.age.notIn(10, 20)
idol.age.between(10,30)
idol.age.goe(30)
idol.age.gt(30)
idol.age.loe(30)
idol.age.lt(30)
idol.idolName.like("_김%")
idol.idolName.contains("김")
idol.idolName.startsWith("김")
idol.idolName.endsWith("김")
QueryDsl세팅
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;
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
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
class QueryDslBasicTest {
@Autowired
IdolRepository idolRepository;
@Autowired
GroupRepository groupRepository;
@Autowired
EntityManager em;
@Autowired
JPAQueryFactory factory;
@BeforeEach
void setUp() {
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() {
Idol foundIdol = factory
.select(idol)
.from(idol)
.where(idol.idolName.eq("사쿠라"))
.fetchOne();
System.out.println("foundIdol = " + foundIdol);
assertEquals("르세라핌", foundIdol.getGroup().getGroupName());
}
}
@Test
@DisplayName("이름과 나이로 아이돌 조회하기")
void searchTest() {
String name = "리즈";
int age = 20;
Idol foundIdol = factory
.select(idol)
.from(idol)
.where(
idol.idolName.eq(name)
.and(idol.age.eq(age))
)
.fetchOne();
System.out.println("foundIdol = " + foundIdol);
assertNotNull(foundIdol);
assertEquals("아이브", foundIdol.getGroup().getGroupName());
}
@Test
@DisplayName("조회 결과 반환하기")
void fetchTest() {
List<Idol> idolList = factory
.select(idol)
.from(idol)
.fetch();
Idol foundIdol = factory
.select(idol)
.from(idol)
.where(idol.age.lt(21))
.fetchOne();
Optional<Idol> foundIdolOptional = Optional.ofNullable(factory
.select(idol)
.from(idol)
.where(idol.age.lt(21))
.fetchOne());
Idol foundIdol2 = foundIdolOptional.orElseThrow();
}