JPA에서 제공하는 주요 조회 방법
JPA는 객체를 기반으로 검색할 수 있도록 여러 가지 방법을 제공합니다.
조회 | 방법설명 |
JPQL (Java Persistence Query Language) | 엔티티 객체를 대상으로 SQL과 유사한 문법으로 조회 |
네이티브 SQL | JPA에서 직접 SQL을 사용할 수 있도록 지원 |
QueryDSL | JPQL을 타입 안전하게 편리하게 작성할 수 있도록 도와주는 빌더 클래스 |
JDBC 직접 사용 / MyBatis 같은 SQL 매퍼 | SQL을 직접 사용하여 조회 성능 최적화 가능 |
객체지향쿼리의 필요성
A를 조회할 때 단순한 EntityManager.find() 또는 객체 그래프 탐색 (a.getB().getC()) 방식으로만 데이터를 조회하는 것은 어려움.
- 모든 회원 엔티티를 메모리에 올려두고 나서 조건에 맞추어 검색하는 것은 비현실적
- 데이터베이스 상에서 sql로 최대한 내용을 필터링하여 가져오는 것이 성능에서 유리함.
- 객체지향성을 유지하면서 필터링을 하는 기능이 필요함.
- 복잡한 작업 처리: JPA의 기본 메서드만으로는 해결하기 어려운 복잡한 데이터 처리 작업을 수행해야 할 때, JPQL 또는 Native Query가 필요합니다.
객체지향 쿼리의 장점
- 데이터베이스 테이블이 아니라 엔티티를 대상으로 검색
- SQL처럼 테이블을 다루지 않고, 객체를 대상으로 쿼리를 작성할 수 있음.
- 따라서 DB에 종속적인 코드가 줄어들어 유지보수가 편리함.
- 컴파일 시점에 문법 오류를 발견 가능
- JPQL은 문자열 기반이므로 런타임에서 오류가 발생할 수 있음.
- 하지만 QueryDSL은 컴파일 시점에 문법 오류를 잡을 수 있어 안정성이 높음.
- 동적 쿼리 작성이 쉬움
- 동적 쿼리는 사용자의 입력값이나 특정 조건에 따라 SQL이 변경되는 쿼리를 의미합니다.
- 검색 기능에서 "사용자가 입력한 값에 따라 WHERE 절을 다르게 적용" 하는 경우가 해당됩니다.
- BooleanBuilder, where() 조건 조합 등을 활용하여 가독성 높은 코드를 작성할 수 있음.
Repository method 커스텀 정의
커스텀 필요성
JPA의 method를 통해서 많은 부분들이 추상화되었지만 여전히 SQL작성은 필요함. 아래와 같은 케이스가 그 케이스임.
커스텀 쿼리 주요 어노테이션
- @Query : hibernate method 외에 JPQL 쿼리를 커스텀으로 작성할 수 있다.
- nativeQuery = true : SQL문을 작성가능(DB 종속적임)
- @Modifying : INSERT, UPDATE, and DELETE문으로 해당 쿼리가 단순 조회가 아닌 데이터를 수정하는 쿼리임을 표시하며, 수정된 행(row)의 수를 반환합니다.
- @Param (쿼리 파라미터 매핑) : JPQL/Native Query 내부의 :parameter과 메소드의 parameter 변수를 매핑
- @EntityGraph(attributePaths = {"relatedEntity"}) : Select 문으로 entity를 가져올 때 연관된 entity를 joinfetch 하여 N+1문제를 예방한다.
- BestPractie : JPA 내장 메소드에 사용시에는 동작을 변경시키기때문에 커스텀 메소드에 적합하며 Join문을 줄여주는 역할함
쿼리DSL 설정
Dependency 설정
// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
Spring에서 기본지원하지 않기 때문에 Bean을 생성해서 주입해줘야한다.
- JPAQueryFactory Configuration (Q Factory)
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
- Define a Custom Interface
public interface UserRepositoryCustom {
List<User> findUsersByName(String name);
}
- Implement the Custom Interface
- QueryDSL Q-type objects : QueryDSL-generated classes that map to your JPA entity classes.
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
import static com.example.entity.QUser.user; // Import the generated Q-class
@Repository
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory; // Injected via Spring
@Override
public List<User> findUsersByName(String name) {
return queryFactory
.selectFrom(user)
.where(user.name.eq(name))
.fetch();
}
}
- Extend Your JPA Repository
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
}
QueryDSL 문법 정리
기능 | QueryDSL | SQL Equivalent |
전체 조회 | selectFrom(auth).fetch() | SELECT * FROM auth; |
특정 컬럼 조회 | select(auth.name).fetch() | SELECT name FROM auth; |
조건 조회 | where(auth.name.eq("ADMIN")) | WHERE name = 'ADMIN' |
AND / OR | where(a.eq(1).and(b.eq(2))) | WHERE a = 1 AND b = 2 |
정렬 | orderBy(auth.name.asc()) | ORDER BY name ASC |
그룹핑 | groupBy(auth.name) | GROUP BY name |
조인 | join(userAuth).on(userAuth.auth.eq(auth)) | JOIN user_auth ON user_auth.auth_id = auth.id |
COUNT | select(auth.count()) | SELECT COUNT(*) |
EXISTS | selectOne().from(auth).where(...).fetchFirst() != null | SELECT 1 WHERE EXISTS(...) |
서브쿼리 | where(auth.id.in(JPAExpressions.select(...))) | WHERE id IN (SELECT ...) |
페이징 | limit(10).offset(5) | LIMIT 10 OFFSET 5 |
JPQL vs Native SQL vs QueryDSL 비교 및 설명
Native SQL
- 네이티브 SQL은 JPQL과 달리 데이터베이스의 테이블을 직접 대상으로 함
- 결과가 엔티티 매핑이 보장되지 않음
- 특정 데이터베이스의 고유 기능(예: JSON 함수, 윈도우 함수, InsertIgnore, OnduplicateUpdate 등)을 사용 가능
@Modifying
@Query(value = "INSERT INTO heritage_entity"
+ " (heritage_Id, heritage_Name, location, heritage_Grade, basic_Description)"
+ " VALUES (:heritageId, :heritageName, :location, :heritageGrade, :basicDescription)"
+ " ON DUPLICATE KEY UPDATE"
+ " heritage_Name = :heritageName, location = :location, heritage_Grade ="
+ " :heritageGrade, basic_Description = :basicDescription", nativeQuery = true)
int insertOrUpdate(String heritageId, String heritageName, Point location,
String heritageGrade, String basicDescription);
JPQL
- SQL과 달리 테이블이 아니라 엔티티(Entity)를 대상으로 쿼리를 작성하여 Select *이 아니라 select entity(or alias) 사용
- 유지보수성 : DB가 엔티티로 추상화되어있기때문에 DB 엔진이나 테이블 구조가 바뀌어도 엔티티 구조에따라 JPQL을 그대로 사용가능
- JOIN FETCH : 테이블을 Join 시키면서 Join하는 테이블을 eager fetch하도록 한다.
@Query("select v.heritageEntity from VisitedHeritageEntity v join fetch v.heritageEntity where v.memberEntity.memberId = :memberId")
List<HeritageEntity> findAllVisitedHeritageByMemberId(String memberId);
// Custom query to calculate the average rating for a specific store
@Query("SELECT AVG(r.rating) FROM ReviewEntity r WHERE r.storeEntity = :storeEntity")
Double findAverageRatingByStore(@Param("storeEntity") StoreEntity storeEntity);
}
QueryDSL (타입 안전한 동적 쿼리 빌더)
JPQL을 Java 코드로 변환하여 더 안전하고, 유지보수하기 쉽게 만든 쿼리 빌더 프레임워크방식. QueryDSL을 사용하면 JPA가 실행할 때 QueryDSL → JPQL → SQL 변환 과정을 거칩니다.
- 동적 쿼리 작성이 편리함 (where() 절에 조건을 동적으로 추가 가능
- 자동 완성 지원 (user.username.eq(username)) → 유지보수성 향상
다양한 쿼리 상황에서 Native SQL, JPQL, QueryDSL 문법 비교
단순 조회 (단건 조회)
@Query(value = "SELECT * FROM user_table WHERE username = :username", nativeQuery = true)
Optional<UserEntity> findByUsernameNative(@Param("username") String username);
- SQL : from 절이 전체 쿼리에 영향을 미치기에 username앞에 테이블을 생략가능. 단, 여러 테이블을 JOIN시에명시
@Query("SELECT u FROM UserEntity u WHERE u.username = :username")
Optional<UserEntity> findByUsername(@Param("username") String username);
QUserEntity user = QUserEntity.userEntity;
Optional<UserEntity> userEntity = queryFactory
.selectFrom(user)
.where(user.username.eq(username))
.fetchOne();
다중 조건 필터링 조회 (동적 쿼리)
(예: username과 role이 일치하는 유저 검색, 조건이 없으면 전체 검색)
@Query(value = "SELECT * FROM user_table WHERE (:username IS NULL OR username = :username) AND (:role IS NULL OR role = :role)", nativeQuery = true)
List<UserEntity> findUsers(@Param("username") String username, @Param("role") String role);
String jpql = "SELECT u FROM UserEntity u WHERE 1=1";
if (username != null) jpql += " AND u.username = :username";
if (role != null) jpql += " AND u.role = :role";
@Query(jpql)
List<UserEntity> findUsers(@Param("username") String username, @Param("role") String role);
QUserEntity user = QUserEntity.userEntity;
BooleanBuilder builder = new BooleanBuilder();
if (username != null) builder.and(user.username.eq(username));
if (role != null) builder.and(user.role.eq(role));
List<UserEntity> users = queryFactory
.selectFrom(user)
.where(builder)
.fetch();
다중 테이블 JOIN
- Native SQL :
- Since the result contains columns from both user_table and order_table, JPA does not know where to map thus object[]
- native queries cannot directly return JPA entities with relationships.
@Query(value = "SELECT u.*, o.* FROM user_table u JOIN order_table o ON u.id = o.user_id WHERE u.username = :username", nativeQuery = true)
List<Object[]> findUserOrdersNative(@Param("username") String username);
- JPQL SQL :
- JOIN FETCH ensures that orders are eagerly loaded into UserEntity.orders
@Query("SELECT u FROM UserEntity u JOIN FETCH u.orders WHERE u.username = :username")
List<UserEntity> findUserOrders(@Param("username") String username);
QUserEntity user = QUserEntity.userEntity;
QOrderEntity order = QOrderEntity.orderEntity;
List<Tuple> userOrders = queryFactory
.select(user, order)
.from(user)
.join(user.orders, order)
.where(user.username.eq(username))
.fetch();
Native SQL :
/*
SELECT a.id AS auth_id, a.name AS auth_name, COUNT(ua.id) AS user_count
FROM auth a
LEFT JOIN user_auth ua ON ua.auth_id = a.id
GROUP BY a.id, a.name;
*/
QueryDSL Code :
public List<AuthorityDto.AuthListResDao> findAllWithUserCount() {
List<Tuple> results = queryFactory
.select(auth.id, auth.name, userAuth.count())
.from(auth)
.leftJoin(userAuth).on(userAuth.auth.eq(auth))
.groupBy(auth.id, auth.name)
.fetch();
'개발기술 > ORM' 카테고리의 다른 글
Persistence Context, EntityManager (0) | 2025.03.18 |
---|---|
JPA 기타기능 (Pageable, auditing, data.sql 기능) (0) | 2025.02.28 |
JPA 활용의 필요성과 활용법 (0) | 2025.02.27 |
Spring JPA Hibernate 트랜잭션 (0) | 2024.12.22 |
Spring JPA Entity 설정 (2) | 2024.10.28 |