반응형
해당 글에서는 QueryDSL에서 Join을 하는 방법에 대해 알아봅니다.
💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 링크 |
Spring Boot Data JPA -1: ORM, JPA, Hibernate, QueryDSL 이론 | https://adjh54.tistory.com/421 |
Spring Boot Data JPA -2: 초기 환경 구성 + JpaRepository 활용 방법 | https://adjh54.tistory.com/422 |
Spring Boot Data JPA -3: 상세 JpaRepository 활용 방법(Query Method, @Query, NamedQuery) | https://adjh54.tistory.com/481 |
Spring Boot Data JPA 엔티티 어노테이션 -1 : 테이블 컬럼 단위 | https://adjh54.tistory.com/466 |
Spring Boot Data JPA 엔티티 어노테이션 -2 : 엔티티(테이블)간의 관계 | https://adjh54.tistory.com/477 |
Spring Boot Data JPA FetchType 이해하기 : 즉시/지연로딩 | https://adjh54.tistory.com/476 |
Spring Boot Data JPA + JPQL 활용 방법 | https://adjh54.tistory.com/479 |
Spring Boot Data JPA + Criteria API 활용 방법 | https://adjh54.tistory.com/483 |
Spring Boot Data JPA + QueryDSL 활용 방법-1 : 정의 및 구성요소 | https://adjh54.tistory.com/484 |
Spring Boot Data JPA + QueryDSL 활용 방법-2 : 초기 환경설정 및 활용예시 | https://adjh54.tistory.com/485 |
Spring Boot Data JPA + QueryDSL 활용 방법-3 : Join 활용하기 | https://adjh54.tistory.com/488 |
1) QueryDSL(Domain-Specific Languages)
💡 QueryDSL
- 다양한 데이터베이스 플랫폼(RDBMS, No-SQL)에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는데 도움을 주는 프레임워크입니다.
- 타입-세이프(type-safe)하게 쿼리를 작성하도록 지원하며 SQL 형태가 아닌 ‘자바 코드’로 작성하여 데이터베이스 쿼리 작성을 쉽고 안전하게 만들어줍니다.
2) QueryDSL Join 메서드 : com.querydsl.jpa.JPAQueryBase 패키지
💡 com.querydsl.jpa.JPAQueryBase 패키지
- QueryDSL 라이브러리의 일부로 JPA 쿼리를 구성하고 실행하는 데 사용되는 클래스입니다.
- 이 클래스는 다양한 종류의 쿼리 메서드를 제공하여, 조인(join), 내부 조인(inner join), 왼쪽 외부 조인(left join), 오른쪽 외부 조인(right join), 패치 조인(fetch join) 등의 쿼리를 구성할 수 있습니다.
- 또한 결과를 가져오기 위한 fetch 메서드도 제공합니다. 이 클래스를 활용하면, SQL 형태가 아닌 '자바 코드'로 데이터베이스 쿼리를 작성하고 실행할 수 있습니다.
1. com.querydsl.jpa.JPAQueryBase 패키지 메서드 종류
메서드 | 리턴 타입 | 설명 |
fetchAll() | Q | 마지막으로 정의된 조인에 "fetchJoin 모든 속성" 플래그를 추가합니다. |
fetchJoin() | Q | 마지막으로 정의된 조인에 "fetchJoin" 플래그를 추가합니다. 컬렉션 조인은 중복 행을 결과로 가져올 수 있으며, "inner join fetchJoin"은 결과 세트를 제한합니다. |
from(CollectionExpression<?,P> target, Path<P> alias) | <P> Q | 쿼리 소스를 추가합니다. |
from(EntityPath<?> arg) | Q | |
from(EntityPath<?>... args) | Q | 이 쿼리에 소스를 추가합니다. |
innerJoin(CollectionExpression<?,P> target) | <P> Q | 주어진 목표로 내부 조인을 생성합니다. |
innerJoin(CollectionExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 내부 조인을 생성합니다. |
innerJoin(EntityPath<P> target) | <P> Q | 주어진 목표로 내부 조인을 생성합니다. |
innerJoin(EntityPath<P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 내부 조인을 생성합니다. |
innerJoin(MapExpression<?,P> target) | <P> Q | 주어진 목표로 내부 조인을 생성합니다. |
innerJoin(MapExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 내부 조인을 생성합니다. |
join(CollectionExpression<?,P> target) | <P> Q | 주어진 목표로 조인을 생성합니다. |
join(CollectionExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표를 사용하여 조인을 생성합니다. 이 조인에 fetchJoin 매개 변수를 추가하려면 fetchJoin()을 사용하세요. |
join(EntityPath<P> target) | <P> Q | 주어진 목표로 조인을 생성합니다. |
join(EntityPath<P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 조인을 생성합니다. |
join(MapExpression<?,P> target) | <P> Q | 주어진 목표로 조인을 생성합니다. |
join(MapExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 조인을 생성합니다. |
leftJoin(CollectionExpression<?,P> target) | <P> Q | 주어진 목표로 왼쪽 조인을 생성합니다. |
leftJoin(CollectionExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 왼쪽 조인을 생성합니다. |
leftJoin(EntityPath<P> target) | <P> Q | 주어진 목표로 왼쪽 조인을 생성합니다. |
leftJoin(EntityPath<P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 왼쪽 조인을 생성합니다. |
leftJoin(MapExpression<?,P> target) | <P> Q | 주어진 목표로 왼쪽 조인을 생성합니다. |
leftJoin(MapExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 왼쪽 조인을 생성합니다. |
on(Predicate condition) | Q | |
on(Predicate... conditions) | Q | 마지막으로 추가된 조인에 조인 조건을 추가합니다. |
getTemplates() | protected JPQLTemplates | |
createSerializer() | protected abstract JPQLSerializer | |
clone() | abstract Q | |
rightJoin(CollectionExpression<?,P> target) | <P> Q | 주어진 목표로 오른쪽 조인을 생성합니다. |
rightJoin(CollectionExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 오른쪽 조인을 생성합니다. |
rightJoin(EntityPath<P> target) | <P> Q | 주어진 목표로 오른쪽 조인을 생성합니다. |
rightJoin(EntityPath<P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 오른쪽 조인을 생성합니다. |
rightJoin(MapExpression<?,P> target) | <P> Q | 주어진 목표로 오른쪽 조인을 생성합니다. |
rightJoin(MapExpression<?,P> target, Path<P> alias) | <P> Q | 주어진 목표와 별칭으로 오른쪽 조인을 생성합니다. |
reset() | protected abstract void | |
serialize(boolean forCountRow) | protected JPQLSerializer | |
serialize(boolean forCountRow, boolean validate) | protected JPQLSerializer | |
toString() | String |
2. Entity Annotation과 .join() 메서드 중 무엇을 사용해야 할까?
💡 Entity Annotation과 .join() 메서드 중 무엇을 사용해야 할까?
- 두 개의 개념은 서로 다릅니다.
- ‘Entity Annotation의 테이블 관계 어노테이션’의 경우는 데이터베이스의 두 개 이상의 테이블이 서로 어떻게 연결되어 있는지에 대한 ‘관계’를 설명합니다. 이는 데이터베이스 설계 단계에서 정의되며 테이블 간의 관계는 일대일(1:1), 일대다(1:N), 다대다(N:M) 등의 형태를 가질 수 있습니다.
- ‘join’의 경우는 두 개 이상의 테이블에서 ‘필요한 데이터를 가져오기 위해 사용하는 방법’을 설명합니다.
- 이를 기반으로 적절한 조인 방법(내부 조인, 외부 조인 등)을 선택합니다.
- 따라서, 엔티티 어노테이션은 테이블 간의 관계를 통해 데이터의 구조를 정의하는 데 사용되며, 조인은 이런 관계를 바탕으로 실제로 데이터를 조회하는 데 사용됩니다.
개념 | 설명 | 사용 시기 |
Entity Annotation | 엔티티 클래스의 필드와 데이터베이스 테이블의 컬럼을 매핑하며, 엔티티 간의 관계를 설정한다. | 엔티티 간의 관계가 애플리케이션의 여러 부분에서 사용되거나 일관성을 유지하는 경우 |
.join() 메서드 | 두 개 이상의 테이블에서 데이터를 검색하는 방법이다. QueryDSL에서는 여러 테이블의 데이터를 하나의 쿼리로 검색할 수 있다. | 쿼리 내에서만 필요한 관계를 정의할 때. 이를 통해 쿼리의 복잡성을 줄일 수 있다. |
💡 [참고] Entity Annotation(엔티티 어노테이션) 종류
어노테이션 | DB 매핑 타입 | 설명 |
@OneToOne | 1:1 관계 매핑 | 엔티티 간 1:1 관계를 정의합니다 |
@OneToMany | 1:N 관계 매핑 | 하나의 엔티티가 다른 엔티티와 1:N 관계를 가짐을 정의합니다 |
@ManyToOne | N:1 관계 매핑 | 하나의 엔티티가 다른 엔티티와 N:1 관계를 가짐을 정의합니다 |
@ManyToMany | N:N 관계 매핑 | 엔티티 간 N:N 관계를 정의합니다 |
@JoinColumns | 여러 외래키 컬럼 매핑 | 복합 키를 매핑할 때 사용합니다 |
@JoinColumn | 외래키 컬럼 매핑 | 외래키를 매핑할 때 사용합니다 |
@JoinTable | N:N 관계 매핑 | 엔티티 간의 N:N 관계를 가질 때 연결 테이블을 매핑할 때 사용합니다 |
💡 [참고] 해당 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
3) QueryDSL Join 종류
💡 QueryDSL Join 종류
- QueryDSL 내에서 사용되는 join에 대해서 각각 종류별로 확인해 봅니다.
메서드 | 설명 |
join | 주어진 목표를 사용하여 조인을 생성합니다. |
innerJoin | 주어진 목표로 내부 조인을 생성합니다. 내부 조인은 두 테이블에서 공통된 값을 가지고 있는 행들만 반환합니다. |
leftJoin | 주어진 목표로 왼쪽 조인을 생성합니다. 왼쪽 조인은 왼쪽 테이블의 모든 행과 오른쪽 테이블의 공통된 행을 반환합니다. |
rightJoin | 주어진 목표로 오른쪽 조인을 생성합니다. 오른쪽 조인은 오른쪽 테이블의 모든 행과 왼쪽 테이블의 공통된 행을 반환합니다. |
fetchJoin | 마지막으로 정의된 조인에 "fetchJoin" 플래그를 추가합니다. 컬렉션 조인은 중복 행을 결과로 가져올 수 있으며, "inner join fetchJoin"은 결과 세트를 제한합니다. |
on | 마지막으로 추가된 조인에 조인 조건을 추가합니다. |
1. 내부 조인(INNER JOIN)
💡 내부 조인(INNER JOIN)
- 두 테이블에서 ‘공통된 값’을 가지고 있는 행들만을 반환합니다.
1.1. 테이블 내부 조인 : 1:1 관계
💡 테이블 내부 조인 : 1:1 관계
- 사용자(tb_user)는 고유한 하나의 여권(tb_passport)을 가지고 있기에 1:1 관계로 구성이 되어 있습니다.
💡 SQL 사용 예시
- 사용자(tb_user) 별 사용자 정보와 여권 정보를 일괄 조회하는 SQL문을 작성합니다.
- tb_passport 테이블과 tb_user 테이블을 내부 조인을 수행하며 user_sq 값이 1인 정보를 조회합니다.
SELECT t2.user_id
, t2.user_nm
, t2.user_st
, t1.passport_id
, t1.expired_date
, t1.issue_date
FROM tb_passport t1
INNER JOIN tb_user t2
ON t1.user_sq = t2.user_sq
WHERE t1.user_sq = 1
💡 QueryDSL 사용 예시
- 위에서 구성한 SQL 사용 예시와 같은 형태로 QueryDSL 내에서 selectUserPassport() 메서드에서 구성하였습니다.
@Repository
public class UserDaoImpl implements UserDao {
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QPassportEntity qPassport = QPassportEntity.passportEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 사용자 정보를 조회합니다.
*
* @param userDto
* @return
*/
@Override
@Transactional(readOnly = true)
public UserPassportDto selectUserPassport(UserDto userDto) {
return queryFactory
.select(Projections.fields(UserPassportDto.class,
qUser.userId,
qUser.userNm,
qUser.userSt,
qPassport.passportId,
qPassport.expiredDate,
qPassport.issueDate
))
.from(qPassport)
.innerJoin(qUser).on(qUser.userSq.eq(qPassport.userSq))
.where(qUser.userSq.eq(userDto.getUserSq()))
.fetchOne();
}
}
💡 수행 결과
- 해당 메서드를 실행하였을 시 아래와 같이 INNER JOIN이 수행되어서 아래와 같은 결과를 얻었습니다.
1.2. 테이블 내부 조인 : 1:N 관계
💡 테이블 내부 조인 : 1:N 관계
- 사용자(tb_user)는 여러 개의 주문정보(tb_order)를 가지고 있을 수 있기에 1 : N 관계로 구성되어 있습니다.
💡 SQL 사용 예시
- 사용자(tb_user) 별 사용자 정보와 모든 주문(tb_order) 정보를 조회하는 SQL문을 작성합니다.
- tb_user 테이블과 tb_order 테이블을 내부 조인을 수행하며 user_sq 값이 1인 정보를 조회합니다.
SELECT t2.user_id
, t2.user_nm
, t2.user_st
, t1.order_sq
, t1.order_nm
, t1.order_req
, t1.order_date
FROM tb_order t1
INNER JOIN tb_user t2
ON t1.user_sq = t2.user_sq
WHERE t1.user_sq = 1;
💡 QueryDSL 사용 예시
- 위에서 구성한 SQL 사용 예시와 같은 형태로 QueryDSL 내에서 selectUserOrderList() 메서드에서 구성하였습니다.
@Repository
public class UserDaoImpl implements UserDao {
// 1. 구성한 JPAQueryFactory를 로드 합니다.
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QPassportEntity qPassport = QPassportEntity.passportEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 사용자의 주문정보를 조회합니다.
*
* @param userDto
* @return
*/
@Override
@Transactional(readOnly = true)
public List<UserOrderDto> selectUserOrderList(UserDto userDto) {
return queryFactory
.select(Projections.fields(UserOrderDto.class,
qUser.userId,
qUser.userNm,
qUser.userSt,
qOrder.orderSq,
qOrder.orderNm,
qOrder.orderReq,
qOrder.orderDate
))
.from(qOrder)
.innerJoin(qUser).on(qUser.userSq.eq(qOrder.userSq))
.where(qUser.userSq.eq(userDto.getUserSq()))
.fetch();
}
}
💡 수행 결과
- 해당 메서드를 실행하였을 시 아래와 같이 INNER JOIN이 수행되어서 아래와 같은 결과를 얻었습니다.
2. 외부 조인(OUTER JOIN : LEFT JOIN)
💡 외부 조인(OUTER JOIN : LEFT JOIN)
- ’ 왼쪽 테이블의 모든 행’과 ‘오른쪽 테이블에서 왼쪽 테이블과 공통된 값’을 가지고 있는 행들을 반환합니다.
- 만약 오른쪽 테이블에서 공통된 값을 가지고 있는 행이 없다면 NULL 값을 반환합니다.
💡 외부 조인 : LEFT JOIN 관계
- 해당 테이블의 관계는 사용자(tb_user), 동아리(tb_club)이라는 테이블과 다대다 관계의 형성을 위한 tb_user_club_map이라는 테이블로 구성되어 있습니다.
- 사용자가 한 명이거나 여러 명일 수 있고, 가입할 수 있는 동아리는 없거나 1개이거나 많을 수 있기에 해당 구조로 구성되어 있습니다.
💡 SQL 사용 예시 : LEFT JOIN
- 해당 SQL문에서는 사용자 별 소속되어 있는 동아리 정보를 조회하기 위한 목적으로 SQL문을 구성하였습니다.
(* 사용자는 동아리를 가입하지 않거나 1개 혹은 여러 개의 동아리에 가입될 수 있습니다)
- 사용자(tb_user)는 반드시 존재해야 하기에 LEFT JOIN을 수행하였습니다.
- 또한 사용자(tb_user)와 동아리(tb_club) 테이블을 이으는 사용자 동아리 맵(tb_user_club_map) 테이블의 데이터가 존재해야 동아리(tb_club) 테이블이 이을 수 있기에 사용자 동아리 맵(tb_user_club_map) 테이블에 LEFT JOIN을 수행하였습니다.
SELECT t1.user_id
, t1.user_nm
, t1.user_sq
, t3.club_nm
, t3.est_date
, t3.club_desc
FROM tb_user t1
LEFT JOIN tb_user_club_map t2
ON t1.user_sq = t2.user_sq
LEFT JOIN tb_club t3
ON t2.club_sq = t3.club_sq
WHERE t1.user_sq = 1;
💡 QueryDSL 사용예시 : LEFT JOIN
- 위에서 구성한 SQL 사용 예시와 같은 형태로 QueryDSL 내에서 selectUserClubListLeft() 메서드에서 구성하였습니다.
@Repository
public class UserDaoImpl implements UserDao {
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QPassportEntity qPassport = QPassportEntity.passportEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QClubEntity qClub = QClubEntity.clubEntity;
private final QUserClubMapEntity qUserClubMap = QUserClubMapEntity.userClubMapEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 사용자와 동아리 정보를 조회합니다. (LEFT JOIN)
*
* @param userDto
* @return
*/
@Override
@Transactional(readOnly = true)
public List<UserClubDto> selectUserClubListLeft(UserDto userDto) {
return queryFactory
.select(Projections.fields(UserClubDto.class,
qUser.userId,
qUser.userNm,
qUser.userSt,
qClub.clubNm,
qClub.estDate,
qClub.clubDesc
))
.from(qUser)
.leftJoin(qUserClubMap).on(qUser.userSq.eq(qUserClubMap.userSq))
.leftJoin(qClub).on(qUserClubMap.clubSq.eq(qClub.clubSq))
.where(qUser.userSq.eq(userDto.getUserSq()))
.fetch();
}
}
💡 수행 결과
- 해당 메서드를 실행하였을 시 아래와 같은 LEFT JOIN이 수행되어서 아래와 같은 결과를 얻었습니다.
3. 외부 조인(OUTER JOIN : OUTER JOIN)
💡 외부 조인(OUTER JOIN : OUTER JOIN)
- Left Join과 반대로 ‘오른쪽 테이블의 모든 행’과 ‘왼쪽 테이블에서 오른쪽 테이블과 공통된 값’을 가지고 있는 행들을 반환합니다.
- 만약 왼쪽 테이블에서 공통된 값을 가지고 있는 행이 없다면 NULL 값을 반환합니다.
💡 외부 조인 : RIGHT JOIN 관계
- 해당 테이블의 관계는 사용자(tb_user), 동아리(tb_club)이라는 테이블과 다대다 관계의 형성을 위한 tb_user_club_map이라는 테이블로 구성되어 있습니다.
- 사용자가 한 명이거나 여러 명일 수 있고, 가입할 수 있는 동아리는 없거나 1개이거나 많을 수 있기에 해당 구조로 구성되어 있습니다.
[ 더 알아보기 ]
💡 Left Join과 Right Join 중 무엇을 많이 사용할까?
- 상황에 따라 다르지만 대체로 'Left Join'을 더 많이 사용합니다. 이는 대부분의 경우 왼쪽 테이블의 데이터를 중심으로 분석하고자 할 때가 많기 때문입니다.
💡 SQL 사용 예시
- 해당 SQL문에서는 사용자 별 소속되어 있는 동아리 정보를 조회하기 위한 목적으로 SQL문을 구성하였습니다.
(* 사용자는 동아리를 가입하지 않거나 1개 혹은 여러 개의 동아리에 가입될 수 있습니다)
- 사용자(tb_user)는 반드시 존재해야 하기에 가장 마지막에 RIGHT JOIN을 수행하였습니다.
- 또한 동아리(tb_club)가 존재하기 위해서는 사용자 동아리 맵(tb_user_club_map)이 존재해야 하기에 다음 RIGHT JOIN을 수행하였고 동아리(tb_club)의 경우는 사용자(tb_user)와 사용자 동아리 맵(tb_user_club_map)이 존재해야 출력이 될 수 있기에 아래와 같이 구성하였습니다.
SELECT *
FROM tb_club t1
RIGHT JOIN tb_user_club_map t2
ON t1.club_sq = t2.club_sq
RIGHT JOIN tb_user t3
ON t2.user_sq = t3.user_sq
WHERE t3.user_sq = 1
💡 QueryDSL 사용 예시
- 위에서 구성한 SQL 사용 예시와 같은 형태로 QueryDSL 내에서 selectUserClubListRight() 메서드에서 구성하였습니다.
@Repository
public class UserDaoImpl implements UserDao {
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QPassportEntity qPassport = QPassportEntity.passportEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QClubEntity qClub = QClubEntity.clubEntity;
private final QUserClubMapEntity qUserClubMap = QUserClubMapEntity.userClubMapEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
/**
* 사용자와 동아리 정보를 조회합니다(RIGHT JOIN)
*
* @param userDto
* @return
*/
@Override
@Transactional(readOnly = true)
public List<UserClubDto> selectUserClubListRight(UserDto userDto) {
return queryFactory
.select(Projections.fields(UserClubDto.class,
qUser.userId,
qUser.userNm,
qUser.userSt,
qClub.clubNm,
qClub.estDate,
qClub.clubDesc
))
.from(qClub)
.rightJoin(qUserClubMap).on(qClub.clubSq.eq(qUserClubMap.clubSq))
.rightJoin(qUser).on(qUserClubMap.userSq.eq(qUser.userSq))
.fetch();
}
}
💡 수행 결과
- 해당 메서드를 실행하였을 시 아래와 같은 RIGHT JOIN이 수행되어서 아래와 같은 결과를 얻었습니다.
4. 패치 조인(FETCH JOIN)
💡 패치 조인(FETCH JOIN)
- JPA에서만 존재하는 조인 방식으로 연관된 엔티티(Entity) 또는 컬렉션(Collection)을 SQL 내에서 한 번에 조회하는 기능을 제공합니다.
- 테이블 간의 관계에서 1:1, 1:다, 다:다 관계에서 모두 사용이 가능하며 FetchType이 지연로딩(Lazy) 형태로 설정되어 있는 경우라도 한 번에 데이터를 조회해오는 방식을 의미합니다.
4.1. 패치 조인 관계 예시
💡 패치 조인 관계 예시
- 사용자 테이블(tb_user)을 기준으로 주문 테이블(tb_order)은 1 : 다 관계입니다. 사용자가 한 명이 있으면 주문 정보는 여러 개가 있기 때문입니다.
- 해당 관계에서 사용자 테이블(tb_user)의 엔티티는 주문 테이블(tb_user)의 엔티티와 @OneToMany 관계를 형성 중입니다.
💡 사용자 엔티티의 구조
- @OneToMany 어노테이션으로 서로의 관계를 구성하였습니다.
- FetchType으로 지연로딩(Lazy)을 이용했기에 UserEntity의 조회를 수행하더라도 OrderEntity의 정보는 가져오지 않습니다.
@Entity
@Getter
@Table(name = "tb_user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment("사용자 시퀀스")
@Column(name = "user_sq")
private long userSq;
@Comment("사용자 아이디")
@Column(name = "user_id")
private String userId;
@Comment("사용자 이름")
@Column(name = "user_nm")
private String userNm;
@Comment("사용자 상태")
@Column(name = "user_st")
private String userSt;
@Comment("동아리 시퀀스")
@Column(name = "club_sq")
private long clubSq;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "userInfo")
private List<OrderEntity> orders;
}
💡 주문 엔티티의 구조
@Entity
@Table(name = "tb_order")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_sq")
@Comment("주문 시퀀스")
private long orderSq;
@Column(name = "user_sq", insertable = false, updatable = false)
@Comment("사용자 시퀀스")
private Long userSq;
@Column(name = "order_nm")
@Comment("주문명")
private String orderNm;
@Column(name = "order_req")
@Comment("주문 요청 정보")
private String orderReq;
@Column(name = "order_date")
@Comment("주문일자")
@CreationTimestamp
private Timestamp orderDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_sq")
private UserEntity userInfo;
}
💡 아래와 같은 메서드를 구성하여서 아래의 쿼리를 구성하여 결과를 수행했습니다.(패치조인 적용 이전)
- 파라미터로 전달받은 userSq 값에 따라 일반적인 select문을 수행합니다.
- 해당 결과로 userEntity의 모든 값을 가져옵니다.
public List<UserEntity> selectUserInfo(UserDto userDto) {
return queryFactory
.selectFrom(qUser)
.where(qUser.userSq.eq(userDto.getUserSq()))
.fetch();
}
💡 패치 조인을 이용하여서 메서드를 구성하여 수행하면 아래와 같이 수행이 됩니다.
- 아래와 같이 join이 수행되는 부분 뒤에 fetchJoin()을 추가해 줍니다.
- 결과로 orderEntity의 값을 모두 가져왔습니다.
public List<UserEntity> selectUserInfo(UserDto userDto) {
return queryFactory
.selectFrom(qUser)
.join(qUser.orders)
.fetchJoin()
.where(qUser.userSq.eq(userDto.getUserSq()))
.fetch();
}
[ 더 알아보기 ]
💡그럼 FETCH JOIN이 사실상 fetchType.EAGER과 동일한 거 아닐까?
- 패치 조인(Fetch Join)과 FetchType.EAGER은 비슷한 개념이지만 완전히 같지는 않습니다.
- FetchType.EAGER은 연관된 엔티티를 즉시 로딩하도록 설정하는 것을 의미합니다. 즉, 주 엔티티를 조회할 때 연관된 엔티티도 함께 조회합니다.
- 패치 조인은 연관된 엔티티를 SQL 한 번으로 함께 조회하는 것을 가능하게 합니다.
5. 세타 조인(THETA JOIN)
💡 세타 조인(THETA JOIN)
- 두 테이블 간에 조인을 수행할 때 사용하는 방법입니다. 이 방법은 ‘두 테이블 간에 공통적인 열이 없어도’ 조인을 수행하는 방식입니다.
- 조인 조건은 보통 비교 연산자를 사용하여 두 테이블의 모든 행 조합 중에서 조인 조건을 만족하는 행만을 반환합니다.
- 예를 들어, Employee 테이블과 Department 테이블이 있고, 이 두 테이블에는 공통적인 열이 없다고 가정해 봅시다. 이 경우, 세타 조인을 사용하여 두 테이블을 조인할 수 있습니다.
- 이를 위해 WHERE 절 내에서 두 테이블의 특정 열을 비교하고, 비교 결과에 따라 조인이 수행됩니다.
- 세타 조인은 연산 비용이 높을 수 있으므로 성능에 주의하고 다른 조인 방법들보다 효율성이 떨어질 수 있습니다.
[ 더 알아보기 ]
💡 세타 조인은 크로스 조인(Cross Join)과 같은 개념인가?
- 두 테이블의 모든 행을 조합한 결과를 반환하는 반면, 세타 조인은 두 테이블 간의 임의의 조건에 따라 조인을 수행합니다.
- 즉, 세타 조인은 조건에 따라 필터링이 가능하지만, 크로스 조인은 두 테이블의 모든 조합을 반환합니다.
💡 세타 조인(THETA JOIN)
- 해당 테이블 간의 관계는 사용자(tb_user)와 동아리(tb_club) 간에 서로 연관이 없는 구조이지만, 사용자 이름과 동아리 회장 이름과의 관계에서 서로 동일한 이름이 있다면 이에 맞는 조건을 반환해 줄 수 있습니다.
💡 SQL 사용 예시
- 해당 SQL문에서는 사용자 테이블과 동아리 테이블 간의 관계가 존재하지 않지만 동일한 이름으로 연관 지어서 사용자와 동아리 테이블 데이터를 조회하는 SQL문을 구성하였습니다.
SELECT *
FROM tb_user t1
, tb_club t2
WHERE t1.user_nm = t2.club_captain_nm
💡 QueryDSL 사용 예시
- 위에서 구성한 SQL 사용 예시와 같은 형태로 QueryDSL 내에서 selectUserClubAllList() 메서드에서 구성하였습니다.
- 세타 조인을 수행하기 위해서는 from 절 내에 Q-Class를 함께 호출하며 WHERE 조건으로 두 테이블 간을 연결 지을 공통 조건을 주어서 조인을 수행합니다.
@Repository
public class UserDaoImpl implements UserDao {
private final JPAQueryFactory queryFactory;
private final QUserEntity qUser = QUserEntity.userEntity;
private final QPassportEntity qPassport = QPassportEntity.passportEntity;
private final QOrderEntity qOrder = QOrderEntity.orderEntity;
private final QClubEntity qClub = QClubEntity.clubEntity;
private final QUserClubMapEntity qUserClubMap = QUserClubMapEntity.userClubMapEntity;
@PersistenceContext
private EntityManager em;
public UserDaoImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public List<UserEntity> selectUserClubAllList(UserDto userDto) {
return queryFactory
.select(qUser)
.from(qUser, qClub)
.where(qUser.userNm.eq(qClub.clubCaptainNm))
.fetch();
}
}
💡 수행 결과
- 해당 메서드를 실행하였을 시 아래와 같은 RIGHT JOIN이 수행되어서 아래와 같은 결과를 얻었습니다.
오늘도 감사합니다. 😀
반응형
'Java > JPA' 카테고리의 다른 글
[Java/JPA] Spring Boot JPA 환경에서 HikariCP 적용하기 (0) | 2024.07.22 |
---|---|
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -4 : 서브쿼리 (2) | 2024.05.17 |
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -2 : 초기 환경설정 및 사용예시 (0) | 2024.05.05 |
[Java/JPA] Spring Boot Data JPA + QueryDSL 이해하기 -1 : 정의 및 구성요소 (0) | 2024.05.04 |
[Java/JPA] Spring Boot Data JPA + Criteria API 이해하기 -1 : 정의 및 기본동작 (0) | 2024.04.24 |