- 다양한 데이터베이스 플랫폼(RDBMS, No-SQL)에 접근하여 SQL과 유사한 문법으로 쿼리를 작성하여 데이터 처리를 수행하는데 도움을 주는 프레임워크입니다. - 타입-세이프(type-safe)하게 쿼리를 작성하도록 지원하며 SQL 형태가 아닌 ‘자바 코드’로 작성하여 데이터베이스 쿼리 작성을 쉽고 안전하게 만들어줍니다.
💡 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"은 결과 세트를 제한합니다.
- ‘Entity Annotation의 테이블 관계 어노테이션’의 경우는 데이터베이스의 두 개 이상의 테이블이 서로 어떻게 연결되어 있는지에 대한 ‘관계’를 설명합니다. 이는 데이터베이스 설계 단계에서 정의되며 테이블 간의 관계는 일대일(1:1), 일대다(1:N), 다대다(N:M) 등의 형태를 가질 수 있습니다.
- ‘join’의 경우는 두 개 이상의 테이블에서 ‘필요한 데이터를 가져오기 위해 사용하는 방법’을 설명합니다. - 이를 기반으로 적절한 조인 방법(내부 조인, 외부 조인 등)을 선택합니다. - 따라서, 엔티티 어노테이션은 테이블 간의 관계를 통해 데이터의 구조를 정의하는 데 사용되며, 조인은 이런 관계를 바탕으로 실제로 데이터를 조회하는 데 사용됩니다.
개념
설명
사용 시기
Entity Annotation
엔티티 클래스의 필드와 데이터베이스 테이블의 컬럼을 매핑하며, 엔티티 간의 관계를 설정한다.
엔티티 간의 관계가 애플리케이션의 여러 부분에서 사용되거나 일관성을 유지하는 경우
.join() 메서드
두 개 이상의 테이블에서 데이터를 검색하는 방법이다. QueryDSL에서는 여러 테이블의 데이터를 하나의 쿼리로 검색할 수 있다.
- 사용자(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의 정보는 가져오지 않습니다.
[ 더 알아보기 ] 💡그럼 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이 수행되어서 아래와 같은 결과를 얻었습니다.