JPQL이란 ?
JPQL(Java Persistence Query Language)은 엔티티 객체를 대상으로 하는 객체지향 쿼리 언어입니다.
엔티티 객체란?
엔티티 객체는 데이터 베이스 테이블과 매핑된 자바 객체를 의미합니다.
================================================================
기본 문법 이해
더보기
SQL vs JPQL 비교
// SQL (테이블 중심)
select * from board_tb where username = 'ssar'
//JPQL (엔티티 중심)
SELECT b FROM b WHERE b.username = 'ssar'
핵심 차이점:
-SQL : 테이블명(board_tb), 컬럼면 사용
- JPQL: 엔티티명(Board_tb), 컬러명 사용
핵심 차이점
SQL : 테이블 (Board), 컬럼명 사용
- JPQL : 엔티티명(Board), 필드명
기본 구조
// JPQL 기본 구조
SELECT [별칭] FROM [엔티티명] [별칭] WHERE [조건]
// 실제 예시
"SELECT b FROM Board b WHERE b.id = :id"
// ↑ ↑ ↑ ↑ ↑ ↑
// SELECT 별칭 엔티티명 별칭 필드명 파라미터
2. 기본 조회 패턴
더보기
전체 조
// 모든 게시글 조회
public List<Board> findAll() {
String jpql = "SELECT b FROM Board B';
return em.createQuery(jpql, Board.class)
.getResultList();
}
// 정렬 추가
public List<Board> findAllOrderByDate() {
String jpql = "SELECT b FROM Board b ORDER BY b.createdAt DESE";
return em.createQuery(jpql, Board.class)
.getReultList();
}
조건 조회
// 특정 사용자의 게시글
public List<Board> findByUsername(String username) {
String jpql = "SELECT b FROM Board b WHERE b.username = :username";
return em.createQuery(jpql, Board.class)
.setParameter("username", username) // 파라미터 바인딩
.getResultList();
}
// 제목에 키워드 포함
public List<Board> findByTitleContaining(String keyword) {
String jpql = "SELECT b FROM Board b WHERE b.title LIKE :keyword";
return em.createQuery(jpql, Board.class)
.setParameter("keyword", "%" + keyword + "%")
.getResultList();
}
// 여러 조건 조합
public List<Board> findByUsernameAndTitle(String username, String titleKeyword) {
String jpql = "SELECT b FROM Board b " +
"WHERE b.username = :username " +
"AND b.title LIKE :title";
return em.createQuery(jpql, Board.class)
.setParameter("username", username)
.setParameter("title", "%" + titleKeyword + "%")
.getResultList();
}
3. 집계 함수와 그룹화
더보기
개수 조회
// 전체 게시글 수
public long countAll(){
String jpql = "SELECT COUNT(b) FROM Board b";
return em.createQuery(jpq;, Long.class)
.getSingleResult();
}
// 특정 사용자의 게시글 수
public long countByUsername(String username) {
String jpql = "SELECCT COUNT(b) FROM b WHERE b.username = :username";
return em.createQuery(jpql, Long.class)
.setParameter("username",username)
.getSingleResult();
}
통계 조회
// 사용자별 게시글 수
public List<Object[]> countByUsernameGroup() {
String jpql = "SELECT b.username, COUNT(b)" +
"FROM Board b " +
"GROUP BY b.username " +
"ORDER BY COUNT(b) DESC ";
return em.createQuery(jpql, Object[].class)
.getReslutList();
// 결과 사용법
// for (object[] result : result) {
// String username = (String) result[0];
// Long count = (Long) result[1];
// }
}
// 평균 조회수
public Double gwtAverageViewCount(){
String jpql = "SELECT AVG(b.viewCount) FROM Board b";
return em.createQuery(jpql,Double.class)
.getSingleResult();
}
4 .페이징 처리
더보기
기본 페이징
// 페이지별 게시글 조회
public List<Board>fingBoardWithPaging(int page, int size) {
String jpql - "SELECT b FROM Board b ORDER BY b. createdAt DESC";
return em.createQuery(jpql, Board.class)
.setFirstResult(page * size) //OFFSET: 시작 위치
.setMaxResults(size) // LIMIT: 조회할 개수
.getResultList();
}
// 예시: 2페이지, 10개씩 조회
// page=1,size=10 -> setFirstResult(10),setMaxResultes(10)
// 결과 : 11~20번째 게시글 조회
페이징 + 검색
// 검색 조건이 있는 페이징
public List<Board> searchWithPaging(String keyword, int page, int size) {
String jpql = "SELECT b FROM Board b " +
"WHERE b.title LIKE :keyword " +
"ORDER BY b.createdAt DESC";
return em.createQuery(jpql, Board.class)
.setParameter("keyword", "%" + keyword + "%")
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
}
5. 날짜 조건 조회
더보기
날짜 범위 조회
// 최근 7일 내 게시글
public List<Board> findRecentBoards(int days) {
String jpql = "SELECT b FROM Board b " +
"WHERE b.createdAt >= :cutoffDate " +
"ORDER BY b.createdAt DESC";
LocalDateTime cutoff = LocalDateTime.now().minusDays(days);
return em.createQuery(jpql, Board.class)
.setParameter("cutoffDate", cutoff)
.getResultList();
}
// 특정 날짜 범위
public List<Board> findBoardsBetweenDates(LocalDateTime startDate, LocalDateTime endDate) {
String jpql = "SELECT b FROM Board b " +
"WHERE b.createdAt BETWEEN :startDate AND :endDate " +
"ORDER BY b.createdAt DESC";
return em.createQuery(jpql, Board.class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
}
6. 조인 쿼리(연관관계 활용)
더보기
기본 조인
// 게시글과 작성자 정보 함께 조회 (N+1 문제 해결)
public List<Board> findBoardsWithUser() {
String jpql = "SELECT b FROM Board b JOIN FETCH b.user u";
return em.createQuery(jpql, Board.class)
.getResultList();
}
// 조건이 있는 조인
public List<Board> findBoardsByUserStatus(String status) {
String jpql = "SELECT b FROM Board b " +
"JOIN b.user u " +
"WHERE u.status = :status";
return em.createQuery(jpql, Board.class)
.setParameter("status", status)
.getResultList();
}
7.서브쿼리 활용
평균보다 높은 조회수
// 평균 조회수보다 높은 게시글
public List<Board> findPopularBoards() {
String jpql = "SELECT b FROM Board b " +
"WHERE b.viewCount > (SELECT AVG(b2.viewCount) FROM Board b2)";
return em.createQuery(jpql, Board.class)
.getResultList();
}
// 댓글이 많은 게시글
public List<Board> findBoardsWithManyComments(int minComments) {
String jpql = "SELECT b FROM Board b " +
"WHERE b.id IN (SELECT c.board.id FROM Comment c " +
" GROUP BY c.board.id " +
" HAVING COUNT(c) >= :minComments)";
return em.createQuery(jpql, Board.class)
.setParameter("minComments", minComments)
.getResultList();
}
8. 성능을 고려한 JPQL
더보기
필요한 필드만 조회(DTO 프로젝션 쿼리)
// DTO 클래스 정의
public class BoardSummaryDTO {
private Long id;
private String title;
private String username;
private LocalDateTime createdAt;
public BoardSummaryDTO(Long id, String title, String username, LocalDateTime createdAt) {
this.id = id;
this.title = title;
this.username = username;
this.createdAt = createdAt;
}
// getter 생략
}
------------------------------------------------------------------------------
// 엔티티 객체 대신 DTO로 조회 (메모리 효율적)
public List<BoardSummaryDTO> findBoardSummaries() {
String jpql = "SELECT new com.tenco.blog.dto.BoardSummaryDTO(" +
"b.id, b.title, b.username, b.createdAt) " +
"FROM Board b " +
"ORDER BY b.createdAt DESC";
return em.createQuery(jpql, BoardSummaryDTO.class)
.getResultList();
}
| 예시
// 전체 엔티티 조회 (10개 게시글)
List<Board> boards = findAll();
메모리 사용량:
- id: 8 bytes × 10 = 80 bytes
- title: 평균 50 bytes × 10 = 500 bytes
- content: 평균 5000 bytes × 10 = 50,000 bytes ← 불필요한 데이터!
- username: 평균 20 bytes × 10 = 200 bytes
- createdAt: 16 bytes × 10 = 160 bytes
- 기타 필드들: 2000 bytes × 10 = 20,000 bytes ← 불필요한 데이터!
총 메모리: 약 70KB
// DTO 프로젝션 조회 (10개 게시글)
List<BoardSummaryDTO> summaries = findBoardSummaries();
메모리 사용량:
- id: 8 bytes × 10 = 80 bytes
- title: 평균 50 bytes × 10 = 500 bytes
- username: 평균 20 bytes × 10 = 200 bytes
- createdAt: 16 bytes × 10 = 160 bytes
총 메모리: 약 1KB (70배 절약!)
프로젝션이란?
프로젝션(projextion)은 원래 수학/기하학 용어로, 고차원 데이터를 저차원으로 투영(변환)하는 것을 의미합니다.
데이터베이스에서는 전체 데이터 중에서 필요한 부분만 선택해소 가져오는 것을 프로젝션이라 합니다.
주의사항과 Best Practice
좋은 예시
// 파라미터 바인딩 사용(SQL Injection 방지)
String jpql = "SELECT b FROM Board b WHERE b.username = :username";
query.setParamerter("username",userInput);
// 타입 안정성 확보
List<Board> boards = em.createQuery(jpql, Board.class).getResultList();
// 적절한 페이징 처리
query.setFirstResult(offset).setMaxResults(limit);
피해야할 예시
// 문자열 연결 (SQL Injection 위험)
String jpql = "SELECT b FROM Board b WHERE b.username = '" + userInput + "'";
// 타입 미지정 (런타임 에러 위험)
List boards = em.createQuery(jpql).getResultList();
// 페이징 없는 전체 조회 (성능 문제)
List<Board> allBoards = em.createQuery("SELECT b FROM Board b", Board.class).getResultList();
728x90
'Spring boot' 카테고리의 다른 글
버전 2- # 3. 게시글 상세보기 만들기 - Persistence Context와 1차 캐시 활용 (0) | 2025.06.24 |
---|---|
버전 2 -2.게시글 목록 보기 - Persistence Context와 JPQL 활용 (0) | 2025.06.23 |
EntityManager 핵심 메서드 요약표 (0) | 2025.06.23 |
버전 2 -1. 게시글 작성 - PersistenceContext 활용 (0) | 2025.06.22 |
버전 2 - 0. 패키지 구조 변경 하기 (0) | 2025.06.22 |