[자바 ORM 표준 JPA 프로그래밍] 9주차 스터디

박서영·2026년 5월 24일

10장. 객체지향 쿼리 언어 (1)

JPA는 복잡한 검색 조건을 사용해 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.

JPQL은 가장 중요한 객체지향 쿼리 언어이다. Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술이다.

10.1 객체지향 쿼리 소개

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다.

  • 식별자로 조회: EntityManager.find()
  • 객체 그래프 탐색: 예) a.getB().getC()

하지만 위의 방법으로는 현실적이고 복잡한 검색이 어렵다.

JPQL의 특징

데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하기에 검색 역시 테이블이 아닌 엔티티 객체를 대상으로 하는 방법.

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음.

SQL: 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리
JPQL: 엔티티 객체를 대상으로하는 객체지향 쿼리. JPA는 JPQL을 분석해 적절한 SQL을 만들어 데이터베이스를 조회 -> 그 결과로 엔티티 객체를 생성해 반환한다.

+) JPA가 공식 지원하는 기능

  • JPQL
    -Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL 사용 가능

+) JPA가 공식 지원은 하지 않는 기능

  • QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음. 비표준 오픈소스 프레임워크.
  • JDBC 직접 사용/MyBatis 같은 SQL 매퍼 프레임워크.

(1) JPQL 소개

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리로 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원.

JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.

또한 JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로인해 SQL보다 코드가 간결하다.

예) 회원 엔티티 (회원 이름이 kim인 엔티티를 조인)

@Entity (name = "Member")
public class Member {
	@Column (name = "name")
    private String username;
}

String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getReulstList();

(2) Criteria 쿼리 소개

Criteria: JPQL을 생성하는 빌더 클래스. 장점은 문자가 아닌 query.select(m).where(...) 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점.

문자열 기반 쿼리와는 다르게 컴파일 시점에 오류를 발견 가능.

Criteria로 작성한 JPQL의 장점

  • 컴파일 시점에 오류를 발견할 수 있음
  • IDE를 사용하면 코드 자동완성을 지원함
  • 동적 쿼리를 작성하기 편함

JPA의 경우 2.0부터 Criteria를 지원함.
예)

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);

//쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();

위를 참고하면 쿼리를 문자가 아닌 코드로 작성한 것을 알 수 있다. 위에서는 m.get("username")을 문자로 작성했지만, 메타 모델을 사용하면 문자가 아니라 코드로 해당 부분을 작성할 수 있다.

메타 모델 API: 자바가 제공하는 어노테이션 프로세서 기능 사용 시, 어노테이션을 분석하여 클래스를 생성 가능함. JPA의 경우 이 기능을 사용해 Member 엔티티 클래스로 부터 Member_라는 Criteria 전용 클래스를 생성하는데 이것을 이르는 말.

메타 모델을 사용하면 온전히 코드만 사용해서 쿼리를 작성할 수 있음.

m.get("username") -> m.get(Member_.username)
  • Criteria가 가진 장점은 많지만, 이를 모두 상쇄할 만큼 복잡하고 장황함. 사용이 불편한 것과 동시에 Criteria로 작성한 코드는 한눈에 잘 들어오지 않는다는 단점 역시 존재한다.

(3) QueryDSL 소개

QueryDSL: Criteria처럼 JPQL 빌더의 역할을 함. 장점은 코드 기반이면서도 단순하고 사용이 쉽다. 또한 작성한 코드 역시 JPQL과 비슷해 한눈에 들어온다. 다만 JPA 표준이 아니고 오픈소스 프로젝트이다.

JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

List<Member> members = 
	query.from(member)
    .where(member.username.eq("kim"))
    .list(member);

QueryDSL 역시 어노테이션 프로세서를 통해 쿼리 전용 클래스를 만들어야함. QMember는 여기서 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스.

(4) 네이티브 SQL 소개

JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 "네이티브 SQL"이라고 함.

JPQL을 사용한다해도 가끔 특정 데이터베이스에 의존하는 기능을 사용하게 될 때가 존재한다.
예) 오라클 데이터베이스에서만 쓰는 CONNECT BY 등의 사용

다만 이런 특정 데이터베이스의 기능은 표준화되어있지 않기에 JPQL에서 사용할 수 없다. 이때 네이티브 SQL을 사용하면 된다.

String sql = "SELECT id, age, team_id, name FROM member WHERE name = "kim";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

(5) JDBC의 직접 사용 / 마이바티스 같은 SQL 매퍼 프레임워크 사용

JDBC 커넥션에 직접 접근하려할 때 JPA는 JDBC 커텍션을 획득하는 API를 제공하지 않기에, JPA 구현체가 제공하는 방법을 사용해야함.

Session session = entityManager.unwrap(Session.class);
session.doWork(new Work () {
	@Override
    public void execute(Connection connection) throws SQLException {
    	//work...
    }
});
  • JPA의 EntityManager에서 하이버네이트 Session을 우선 구현해야함
  • 이후 Session의 doWork() 메소드를 호출해야함.

JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시(flush)해야함. 둘 모두 JPA를 우회해서 데이터베이스에 접근하기에 JPA는 인식할 수 없음. 따라서 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손하게 될 수 있으니 주의해야함.

  • 참고: 스프링프레임워크 사용시 JPA와 마이바티스를 쉽게 통합 가능함. 또한 스프링프레임워크의 AOP를 적절히 활용해 JPA를 우회해 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 해결 가능.

10.2 JPQL

JPQL의 특징

  • 객체지향 쿼리 언어. 즉, 테이블 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리
  • JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 의존하지 않음.
  • JPQL은 결국 SQL로 변환됨.

(1) 기본 문법과 쿼리 API

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있음. 엔티티 저장 시에는 EntityManager.persist() 메소드를 사용하면 되기에 INSERT문은 없음.

select_문 ::=
	select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
  • JPQL에서 UPDATE, DELETE문은 벌크 연산이라고 함.

SELECT문

SELECT m FROM Meber AS m WHERE m.username = "Hello"
  • 대소문자 구분:
    엔티티 속성은 대소문자를 구분. 반면 JPQL 키워드(SELECT, FROM, AS)는 대소문자를 구분하지 않음.
  • 엔티티 이름:
    JPQL에서 사용한 Member는 클래스명이 아닌 엔티티명이다. 엔티티명의 경우 @Entity(name = "XXX");를 통해 설정할 수 있음. 그러지 않을 경우 클래스명을 기본값으로 사용. 기본값을 사용하는 것이 추천됨.
  • 별칭은 필수
    Member AS m을 살펴보면 Member에 m이라는 별칭을 부여했는데 JPQL의 경우에는 별칭이 필수적임. 별칭이 없으면 오류 발생.

TypeQuery, Query

작성한 JPQL의 실행을 위해서는 쿼리 객체가 필요하다. 이때 쿼리 객체로는 TypeQueryQuery가 존재함. 반환 타입을 명확히 지정할 수 있을 때, TypeQuery 객체를 사용하고 그렇지 않을 때는 Query 객체를 사용하면 된다.

예)

TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = query.getResultList();
for (Member member: resultList) {
	System.out.println("member = " + member);
}

em.createQuery()의 두 번째 파라미터에 반환할 타입을 지정할 때는 TypeQuery를, 그렇지 않을 때에는 Query를 반환함. 조회 대상이 Member 엔티티이기에 대상 타입이 명확한데 이럴 때에는 TypeQuery를 사용할 수 있는 것이다.

Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();

for (Object o: resultList) {
	Object [] result = (Object[]) o;
    System.out.println("username = "+result[0]);
    System.out.println("age = " + result[1]);
}

위의 경우에는 조회대상이 String 타입과 Integer 타입으로 조회 대상 타입이 명확하지 않다. SELECT 절에서 여러 엔티티나 컬럼을 선택하는 경우에는 Query 객체를 사용해야함.

Query 객체는 SELECT절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환, 하나면 Object를 반환함.

결과 조회

아래 메소드를 호출해 실제 쿼리를 실행 및 데이터베이스를 조회하게된다.

  • query.getResultList(): 결과를 예제로 반환. 결과가 없으면 빈 컬렉션을 반환.
  • query.getSingleResult(): 결과가 정확히 하나일 때 사용.
    • 결과가 없으면 ..NoResultException예외 발생
    • 결과가 1개보다 많으면 ..NonUniqueResultException예외 발생

이때 getSingleResult()는 결과가 정확히 1개가 아니면 예외를 발생함.

(2) 파라미터 바인딩

JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩 역시 지원함.

이름 기준 파라미터

이름 기준 파라미터: 파라미터를 이름으로 구분하는 방법. 이름 기준 파라미터는 앞에 :를 사용.

:username이라는 이름 기준 파라미터를 정의하고 query.setParameter()에서 username이라는 이름으로 파라미터를 바인딩.

JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어 연속해서 작성할 수 있음.

List<Member> members = 
em. createQuery("SELECT m FROM Member m where m.username = :username, Member.class)
    .setParameter("username", usernameParam)
    .getResult();

위치 기준 파라미터

위치 기준 파라미터를 사용하기 위해서는 ? 다음에 위치값을 주면 된다. 위치 값은 단 1부터 시작한다.

List<Member> members =
	em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
    	.setParameter(1, usernameParam)
        .getResultList();

위치 기준 파라미터는 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.

(3) 프로젝션

프로젝션: SELECT절에 조회할 대상을 지정하는 것.

SELECT {프로젝션 대상} FROM

엔티티 프로젝션

SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀

처음은 회원을 조회, 두 번째에는 회원과 연관된 팀을 조회하는데 둘 다 엔티티를 프로젝션 대상으로 사용함. 쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야하는 SQL과는 차이가 존재한다.

이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

임베디드 타입 프로젝션

JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용됨. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 존재.

String query = "SELECT a FROM Address a";

따라서 위의 코드는 잘못된 쿼리이며 아래처럼 사용해야함.

String query = "SELECT o.address FROM Order o";
List<Address> address = em.createQuery(query, Address.class).getResultList();

임베디드 타입은 엔티티 타입이 아닌 값 타입. 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음.

스칼라 타입 프로젝션

스칼라 타입: 숫자, 문자, 날짜와 같은 기본 데이터 타입.

List<String> username = 
	em.createQuery("SELECT username FROM Member m", String.class)
    .getResultList();

중복 데이터 제거 시에는 DISTINCT를 사용.

SELECT DISTINCT username FROM Member m

다음과 같은 통계 쿼리도 주로 스칼라 타입으로 조회한다.

Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class)
	. getSingleResult();

여러 값 조회

엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터만 선택해 조회해야할 때가 존재함. 이럴 때에는 TypeQuery는 사용할 수 없기에 Query를 사용해야함.

Query query =
	em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
	Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}

제너릭에 Obejct[]를 사용하면 아래처럼 더 간결하게 개발할 수 있다.

List<Object[]> resultList =
	em.createQuery("SELECT m.username, m.age FROM Member m")
    .getResult();
    
for (Object[] row : resultList) {
	String username = (String) row[0]
}

스칼라 뿐만 아니라 엔티티 타입 역시 여러 값을 함께 조회할 수 있다.

NEW 명령어

앞에서는 여러 필드를 프로젝션할 경우 타입 지정이 불가해 TypeQuery를 사용하지 못해 Object[]를 반환받았지만, 실제 애플리케이션에서는 Object[]를 직접 사용하지 않고 UserDTO와 같은 객체로 변환해 사용하게 된다.

List<UserDTO> userDTOs = new ArrayList<>();
for (Object[] row:resultList) {
	UserDTO userDTO = new UserDTO((String) row[0], (Integer) row[1]);
    userDTOs.add(userDTO);
}

return userDTOs;

public class UserDTO {
	private String username;
    private int age;
    
    public UserDTO (String username, int age) {
    	this.username = username;
        this.age = age;
    }
}

NEW 명령어를 사용하는 예)

TypedQuery<UserDTO> query = 
	em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
    				FROM Member m", UserDTO.class);
    List<UserDTO> resultList = query.getResultList();

SELECT 이후에 NEW 명령어를 사용 시 반환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 또한 NEW 명령어를 사용해 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있음.

주의사항

  • 패키지명을 포함한 전체 클래스명을 입력해야함
  • 순서와 타입이 일치하는 생성자가 필요.

(4) 페이징 API

페이지 처리용 SQL을 작성하는 것은 귀찮기도 하고 데이터베이스마다 이 페이징 처리 문법이 다르다는 문제가 존재함.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
  • setMaxResult(int maxResult): 조회할 데이터 수
TypedQuery<Member> query = 
	em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
    
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();

데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다.

MySQL의 페이징

SELECT
	M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID, 
    M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME DESC LIMIT ?,?

데이터베이스마다 SQL이 다르고, 오라클과 SQLServer의 경우에는 페이징 쿼리를 따로 공부해야 작성할 수 있을 정도로 복잡함. ?에 바인딩하는 값 역시도 데이터베이스마다 다르며 적절한 값을 입력해야함.

(5) 집합과 정렬

집합 => 집합함수와 함께 통계 정보를 구할 때 사용.

집계함수

  • COUNT: 결과의 수를 구함. 반환은 Long 타입.
  • MAX, MIN: 최대, 최소 값을 구함. 문자, 숫자, 날짜 등에 사용.
  • AVG: 평균값을 구함. 숫자타입만 사용할 수 있음. 반환은 Double 타입.
  • SUM: 합을 구함. 숫자타입만 사용 가능하면 반환은 정수합, 소수합.. 등.

집계함수 사용 시 참고사항

  • 널(NULL)값은 무시하기에 통계에 잡히지 않음. (DISTINCT 정의가 되어있어도)
  • 값이 없는데 SUM, AVG, MAX, MIN을 사용하면 널값을 반환. COUNT의 경우에는 0.
  • DISTINCT를 집계함수 안에 사용하면 중복값을 제거하고 집합을 구할 수 있음.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원x

GROUP BY, HAVING

  • GROUP BY: 통계 데이터를 구할 때 특정 그룹끼리 묶어줌.
  • HAVING: GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링.

이런 쿼리들을 리포팅 쿼리/통계 쿼리라함. 결과가 많을 때에는 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해 결과를 보관하는 것이 좋음.

정렬 (ORDER BY)

ORDER BY: 결과를 정렬할 때 사용. 내림차순(DESC) 또는 오름차순(ASC) 선택.

(6) JPQL 조인

JPQL의 조인은 SQL과 기능은 같고 문법만 약간 다르다.

내부조인

내부조인은 INNER JOIN을 사용. INNER는 생략이 가능하다.

String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t " + "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
	.setParameter("teamName". teamName);
    .getResultList();

회원과 팀 내부 조인을 통해 팀A에 소속된 회원을 조회하는 JPQL 예)

SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName

JPQL 조인의 특징

  • 연관 필드를 사용함: m.team이 예로, 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말함.
  • FROM Member m: 회원을 선택하고 m이라는 별칭 부여
  • Member m JOIN m.team t: 회원이 가지고 있는 연관 필드로 팀과 조인. 조인한 팀에 t라는 별칭 부여.

JPQL 조인을 SQL 조인처럼 사용하면 문법 오류 발생. JPQL은 JOIN 명령어 다음 조인할 객체의 연관 필드를 사용.

외부조인

SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t

컬렉션 조인

일대다 관계 또는 다대다관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라함.

  • [회원 -> 팀]으로의 조인: 다대일 조인이면서 단일값 연관 필드를 사용.
  • [팀 -> 회원]으로의 조인: 일대다 조인이면서 컬렉션값 연관 필드를 사용.
SELECT t,m FROM Team t LEFT JOIN t.members m

t LEFT JOIN t.members는 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부조인

세타 조인

WHERE절을 사용해 세타 조인이 가능함. 세타 조인은 내부 조인만을 지원. 세타 조인 사용시에는 전혀 관계없는 엔티티도 조인할 수 있음.

select count(m) from Member m, Team t
where m.username = t.name

JOIN ON

조인할 때 ON절을 지원. ON절 사용 시에는 조인 대상을 필터링하고 조인할 수 있음.

  • 참고: 내부조인의 ON 절은 WHERE절을 사용할 때와 결과가 같기에 보통 ON절은 외부 조인에서만 사용함.
select m, t from Member m
left join m.team t on t.name = 'A'

t.name = 'A'로 조인시점에 조인 대상을 필터링함.

페치조인

페치(fetch)조인: SQL의 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어를 통해 사용 가능.

  • 엔티티 페치조인
select m 
from Member m join fetch m.team

join 뒤에 fetch: 연관된 엔티티나 컬렉션을 함께 조회. (여기서는 회원과 팀을 함께 조회). JPQL 조인과 다라ㅡ게 m.team 뒤에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없음.

엔티티 페치 조인 JPQL에서 select m을 통해 회원 엔티티만 선택하였지만, 실행된 SQL에 따르면 회원과 연관된 팀도 함께 조회된 것을 볼 수 있음.

String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class)
	.getResultList();
    
for (Member member:members) {
	System.out.println("username = "+member.getUsername() + "," + 
    "teamname = " +member.getTeam().name());
}

회원을 조회할 때 페치 조인을 사용해 팀도 함께 조회했기에 연관 팀은 프록시가 아닌 실제 엔티티. 따라서 연관된 팀을 사용해도 지연로딩이 일어나지 않음. 또한, 실제 엔티티이기에 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있음.

컬렉션 페치 조인

일대다 관계인 컬렉션을 페치 조인

select t
from Team t join fetch t.members
where t.name = '팀A'

팀을 조회하면서 페치 조인을 사용해 연관된 회원 컬렉션(t.members)도 함께 조회.

컬렉션 페치 조인한 JPQL에서 select t로 팀만 선택했는데 실행 SQL을 보면 팀과 연관된 회원 역시 함께 조회함. 또한 TEAM 테이블에서 팀A는 하나지만 회원 테이블과 조인하면서 결과가 증가하여 팀A가 2건 조회됨.

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론, 애플리케이션에서 한 번 더 중복을 제거.
select distinct t
from Team t join fetch t.members
where t.name = '팀A'

페치 조인과 일반 조인의 차이

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않음. 팀과 회원을 조인한 후에 팀을 조회한다고 해도, 회원 컬렉션은 조회하지 않음. 즉, 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환함.

반면, 페치 조인은 연관된 엔티티까지 함께 조회함.

페치 조인의 특징과 한계

페치 조인을 사용할 때에는 SQL 한 번으로 연관된 엔티티들을 함께 조회해 SQL 호출 횟수를 줄여 성능을 최적화할 수 있음.

글로벌 로딩 전략: 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치기에 글로벌 로딩 전략이라 부름. 글로벌 로딩 전략을 지연로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치조인을 적용해 함께 조회함.

최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 발생함. 일부 빠를 수는 있지만, 전체적으로 볼 때, 사용하지 않은 엔티티를 자주 로딩하기에 성능에 악영향을 미칠 수 있음.
따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요할 때 페치 조인을 하는 것이 효과적.

  • 페치 조인의 한계
    • 페치 조인 대상에는 별칭을 줄 수 없음.

    • 둘 이상의 컬렉션을 페치할 수는 없음. 구현체에 따라 가능할 때도 있지만, 이때 카티시안 곱이 만들어지기도 하기에 주의해야함.

    • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없음.

      • 컬렉션(일대다)이 아닌 단일값 연관필드들은 페치 조인을 해도 페이징 API 사용 불가.
      • 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기며 메모리에서 페이징 처리를 함. 데이터가 적으면 상관없지만, 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험함.

=> 페치 조인은 성능 최적화에 유용해 실무에서 자주 사용한다. 객체 그래프를 유지할 때 특히 효과적이지만, 여러 테이블을 조인해 엔티티가 가진 모양이 전혀 다른 결과를 내야할 때는 억지로 페치 조인을 사용하기 보다는 여러 테이블에서 필요한 필드들만 조회해 DTO로 반환하는 것이 더 효과적일 수 있음.

(8) 경로 표현식

경로표현식: .(점)을 찍어 객체 그래프를 탐색하는 것을 말함.

용어 정리

  • 상태 필드: 단순히 값을 저장하기 위한 필드 (필드 또는 프로퍼티)
  • 연관 필드: 연관관계를 위한 필드. 임베디드 타입 포함 (필드 또는 프로퍼티)
  • 단일 값 연관 필드: @ManyToOne, @OneToOne 대상 엔티티
  • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany
@Entity
public class Member {
	@Id @GeneratedValue
    private Long id; 
    
    @Column (name = "name")
    private String username; //상태필드
    private Integer age; //상태필드
    
    @ManyToOne(...)
    private Team team; //연관필드
    
    @OneToMany(...)
    private List<Order> orders; //연관필드
}

경로표현식과 특징

JPQL에서 경로 표현식을 사용해 경로 탐색을 하기 위해서는 아래 3가지 경로에 따라 어떤 특징이 있는지 이해해야함.

  • 상태 필드 경로: 경로 탐색의 끝으로 더는 탐색이 불가능함.

  • 단일 값 연관 경로: 묵시적으로 내부 조인이 발생. 단일 값 연관 경로는 계속 탐색 가능.

  • 컬렉션 값 연관 경로: 묵시적으로 내부 조인이 발생하며 더는 탐색할 수 없다. FROM절에서 조인을 통해 별칭을 얻는 경우에는 별칭으로 탐색이 가능하다.

  • 상태 필드 경로 탐색
    select m.username, m.age from Member m

  • 단일 값 연관 경로 탐색
    select o.member from Order o

묵시적 조인: 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는 것을 이르는 말. 묵시적 조인은 모두 내부 조인.

  • 명시적 조인: JOIN을 직접 적어주는 것
    SELECT m FROM Member m JOIN m.team t

  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것. 내부 조인만 가능하다.
    SELECT m.team FROM Member m

  • 컬렉션 값 연관 경로 탐색
    JPQL을 다루면서 가장 많이 하는 실수. t.members처럼 컬렉션까지는 경로탐색이 가능하지만, t.members.username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않음.
    컬렉션에서 경로 탐색을 하려면 조인을 사용해 새로운 별칭을 획득해야함

경로 탐색을 사용한 묵시적 조인 시 주의사항

경로 탐색 사용 시 묵시적 조인이 발생해 SQL에서 내부 조인이 발생할 수 있음.
주의사항:

  • 항상 내부조인이다.
  • 컬렉션은 경로 탐색의 끝. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해 별칭을 얻어야함.
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 줌.

=> 성능이 중요할 때는 분석이 쉽도록 묵시적 조인보다 명시적 조인을 사용하는 것이 낫다.

(9) 서브쿼리

JPQL에서도 서브쿼리를 지원해준다. 하지만 제약이 존재한다.
서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고, SELECT, FROM 절에서는 사용이 불가하다.

서브쿼리 함수

  • [NOT] EXISTS: 서브쿼리에 결과가 존재(또는 존재하지 않으면) 참.
  • {ALL | ANY | SOME}: 비교 연산자와 함께 사용하며
    • ALL: 조건을 모두 만족하면 참
    • ANY 또는 SOME: 같은 의미로 조건을 하나라도 만족하면 참.
  • [NOT] IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참. IN은 서브쿼리가 아닌 곳에서도 사용 가능.

(10) 조건식

타입표현

  • 문자: 작은 따옴표 사이에 표현. 작은 따옴표 표현을 위해서는 작은 따옴표를 두 번 사용.
  • 숫자: L(Long), D(Double), F(Float) 지정.
  • 날짜: DATE{d 'yyyy-mm-dd'} TIME{t 'hh-mm-ss'}
  • Boolean: TRUE, FALSE
  • Enum: 패키지명 포함한 전체 이름 예) jpabook.MemberType.Admin
  • 엔티티 타입: 엔티티 타입을 표현. 예) TYPE(m) = Member

연산자 우선순위

  1. 경로 탐색 연산: (.)
  2. 수학 연산: +, -, *, /,
  3. 비교 연산: =, >=, >, <=, <
  4. 논리 연산: AND, NOT, OR

컬렉션 식

컬렉션에서만 사용하는 특별한 기능.

  • 빈 컬렉션 비교식
    {컬렉션 연관 경로} IS [NOT] EMPTY

컬렉션은 컬렉션 식만 사용할 수 있음.

  • 컬렉션 멤버 식
    {엔티티/값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}

스칼라 식

스칼라는 숫자, 문자, 날짜, case, 엔티티 타입 같은 기본적인 타입을 말함.
수학식, 문자함수, 수학함수, 날짜함수 등을 사용할 수 있음.

CASE 식

특정 조건에 따라 분기할 때 사용하며 4가지가 존재함.

  • 기본 CASE
  • 심플 CASE
  • COALESCE
  • NULLIF

기본 case:

CASE
	{WHEN 조건식 THEN 스칼라식}
    ELSE 스칼라식
END

심플 case:

CASE {조건대상}
	WHEN 스칼라식 THEN 스칼라식
    ELSE 스칼라식
END

COALESCE:

	COALESCE(스칼라식, 스칼라식,...)

스칼라식을 차례대로 조회해 null이 아니면 반환

NULLIF

NULLIF (스칼라식, 스칼라식)

두 값이 같으면 널을 반환하고, 다름녀 첫 번째 값을 반환 보통 집합함수와 함께 사용.

(11) 다형성 쿼리

JPQL로 부모 엔티티 조회 시 그 자식 엔티티도 함께 조회함.

TYPE

TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용.

예)

select i from Item i
where type(i) IN (Book, Movie)

이는 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.

(12) 사용자 정의 함수 호출

JPA 2.1부터 사용자 정의 함수를 지원한다.
예)

function_invocation ::= FUNCTION (function_name {, function_arg}*)
select function('group_concat', i.name) from Item i

하이버네이트 구현체 사용시에는 방언 클래스를 사용해 구현하고 사용할 데이터베이스 함수를 미리 등록해야함.

(13) 기타 정리

  • enum은 비교연산만 지원
  • 임베디드 타입은 비교를 지원하지 않음

EMPTY STRING

JPA 표준은 ''을 길이가 0인 empty string으로 정했지만, 데이터베이스에 따라 ''를 null로 사용하기도 하니 확인하고 사용해야함.

NULL

  • 조건을 만족하는 데이터가 하나도 없을 때
  • NULL은 안ㄹ 수 없는 값으로 모든 수학적 계산의 결과가 NULL이 됨.
  • Null == Null은 알 수 없는 값.
  • Null is Null은 참.

(14) 엔티티 직접 사용

기본키 값

객체 인스턴스는 참조값으로 식별하고 테이블 로우는 기본키 값으로 식별.
따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본키 값을 사용함.

select count(m.id) from Member m //엔티티 아이디를 사용
select count(m) from Member m //엔티티 직접 사용

아래처럼 엔티티의 별칭을 직접 넘겨주게되면 JPQL이 SQL로 변환될 때, 해당 엔티티의 기본키를 사용한다.

외래키 값

특정 팀에 소속된 회원을 찾기위해 팀의 기본키 값을 파라미터로 하여 탐색을 하면, m.team(멤버의 팀) 부분이 외래키와 매핑되어있기에 회원과 팀 사이에 묵시적 조인 없이 데이터를 가져올 수 있다.

(15) Named 쿼리: 정적 쿼리

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있음.

  • 동적 쿼리: em.createQuery("select ...")처럼 JPQL을 문자로 완성해 직접 넘기는 것을 동적 쿼리라함.
  • 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해 필요할 때 사용할 수 있는데 이것을 Named 쿼리라함. 이는 한 번 정의하면 변경할 수 없는 정적인 쿼리.

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고, 사용 시점에 파싱된 결과를 재사용하므로 성능상의 이점도 존재한다. 또한 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스 조회 성능 최적화에도 도움이 됨.

Named 쿼리는 @NamedQuery 어노테이션을 사용해 자바 코드에 작성하거나 XML 문서에 작성할 수 있음.

Named 쿼리를 어노테이션에 정의

Named 쿼리는 쿼리에 이름을 부여해 사용하는 방법.

@Entity
@NamedQuery {
	name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
} public class Member {

}

@NamedQuery.name에 쿼리 이름을 부여하고, @NamedQuery.query에 사용할 쿼리를 입력.

하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 @NamedQueries 어노테이션을 사용하면 됨.

  • @NamedQuery 어노테이션
    • lockMode: 쿼리 실행 시 락을 건다.
    • hints: JPA 구현체에게 제공하는 힌트로 2차 캐시를 다룰 때 사용하거나 한다.

Named 쿼리를 XML에 정의

JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있음. 어노테이션을 사용하는 것이 직관적이고 편리한편이지만, Named 쿼리 작성 시에는 XML을 사용하는 것이 직관적이고 편리함.

xml에 정의한 후에는 MEAT-INF/persistence.xml에 코드를 추가해 주어야함.

<persistence-unit name="jpabook">
	<mapping-file>META-INF/ormMember.xml</mappingfile>

10.3 Criteria

Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API. 이를 사용하면 문자가 아닌 코드로 JPQL을 작성하기 때문에 문법 오류를 컴파일 단계에서 잡을 수 있고 , 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성한다는 장점이 존재. 다만 코드가 복잡하고 장황해 이해가 힘들다는 단점 역시 존재한다.

(1) Criteria 기초

예)

//JPQL: select m from Member m

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더
CriteriaQuery<Member> cq =cb.createQuery(Member.class)

Root<Member> m = cq.from(Member.class);
cq.select(m);

TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();

모든 회원 엔티티를 조회하는 JPQL의 Criteria로 작성.

  • Criteria 쿼리 생성을 위해서는 Criteria 빌더가 필요. 빌더는 EntityManger 또는 EntityManagerFactory에서 얻을 수 있음.
  • Criteria 쿼리 빌더에서 Criteria 쿼리르 새엇ㅇ. 이때 반환 타입 지정 가능.
  • FROM 절을 생성. 반환된 값은 Criteria에서 사용하는 특별한 별칭. m을 조회의 시작이라는 의미로 쿼리 루트라함.
  • SELECT 절을 생성.

아래는 WHERE절과 ORDER BY를 넣어주는 경우.

//JPQL : select m from Member m
//		 where m.username = '회원1'
//		 order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더 생성

//Criteria 생성, 반환 타입 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class); //FROM절-쿼리루트 반환

//검색 조건 정의
Predicate usernameEqual = cb.equal(m.get("username"), "회원1");

//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));

//쿼리 생성
cq.select(m) //SELECT절
  .where(usernameEqual) //WHERE절 생성
  .orderBy(ageDesc); //ORDER BY절 생성
  
  
TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();

쿼리 루트와 별칭

 **쿼리 루트와 별칭**
- Root<Member> m = cq.from(Member.class): 여기서 m이 쿼리 루트.
  조회의 시작점이 되며, Criteria에서 사용되는 특별한 별칭. 이 별칭은 엔티티에서만 부여할 수 있음.
  
Criteria는 코드로 JPQL을 완성하는 도구이기에 경로 표현식 역시 존재한다.
- `m.get("username")`은 JPQL의 `m.username`과 같음.
- `m.get("team").get("name")`는 JPQL의 `m.team.name`과 같음.

(2) Criteria 쿼리 생성

CriteriaBuilder.createQuery() 메소드를 사용해 Criteria 쿼리를 생성하면 됨.

public interface CriteriaBuilder {
	CriteriaQuery<ObjecT> createQuery(); //조회값 반환 타입
    
    <T> CriteriaQuery<T> createQuery(Class<T> resultClass);
    CriteriaQuery<Tuple> createTupleQuery(); //조회값 반환 타입 Tuple
}

Criteria 쿼리 생성 시에는 파라미터로 쿼리 결과에 대한 반환 타입을 지정할 수 있음. CriteriaQuery 생성 시 Member.class를 반환 타입으로 지정한다면, em.createQuery(cq)에서 반환 타입을 지정하지 않아도 됨.

  • 반환타입을 지정할 수 없거나 반환 타입이 둘 이상일 때는 타입을 지정하지 않고 Object로 반환을 받으면 됨.
  • 반환 타입이 둘 이상이면 Object[]를 사용하는 것이 편리하다.
  • 반환 타입을 튜플로 받고 싶을 때에서는 튜플을 사용하면 CreateQuery<Tuple>을 사용하면 된다.

(3) 조회

SELECT 절을 만드는 부분인 select()에 관련한 내용이다.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
	//한 건 지정
    CriteriaQuery<T> select(Selection<? extends T> selection);
    
    //여러 건 지정
    CriteriaQuery<T> multiselct(Selection<?> ... selection)s;
    
    //여러 건 지정
    CriteriaQuery<T> multiselect(List<Selection<?>> selectionList);
}

조회 대상을 한 건, 여러 건 지정

select에 조회 대상을 하나만 지정할 때는 아래처럼 작성함.

cq.select(m);

조회 대상을 여러 건 지정하려면 multiselect를 사용한다.

//JPQL: select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));

여러 건을 지정할 때는 cb.array를 사용해도 된다.

CriteriaBuilder cb =em.getCriteriaBuilder();
//JPQL: select m.username, m.age
cq.select (cb.array(m.get("username"), m.get("age")) );

DISTINCT

distinct는 select, multiselect 뒤에 distinct(true)를 사용하면 됨.

//JPQL: select distinct m.username, m.age
cq.multiselect(m.get("username"), m.get("age")).distinct(true);

NEW, construct()

JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, ...)로 사용함.

<Y> CompoundSelection<Y> construct(Class<Y> resultClass,
Selection<?> ... selections)
//JPQL: select new jpabook.domain.MemberDTO(m.username, m.age)
//from Member m

CriteriaQuery<MemberDTO> cq = cb.createQuery(MemberDTO.class);
Root<Member> m = cq.from(Member.class);

cq.select(cb.construct(MemberDTO.class), m.get("username"),
m.get("age")));

TypedQuery<MemberDTO> query = em.createQuery(cq);
List<MemberDTO> resultList = query.getResultList();

튜플

Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공한다.

튜플을 사용하기 위해서는 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria를 생성.

튜플은 이름 기반이기에 순서 기반인 Object[]보다 안전하며, tuple.getElements() 같은 메소드를 사용해 현재 튜플의 별칭과 자바 타입 역시 조회할 수 있다.

튜플을 사용해 엔티티도 조회할 수 있으면 튜플 사용시에는 별칭을 필수로 주어야한다.

(4) 집합

GROUP BY

cq.groupBy(m.get("team").get("name"));

HAVING

cq.multiselect(m.get("team").get("name"), maxAge, minAge)
	.groupBy(m.get("team").get("name"))
    .having(cb.gt(minAge, 10)); //HAVING

(5) 정렬

정렬 조건 역시 Criteria 빌더를 통해 생성할 수 있다.

//cb.desc(...) 또는 cb.asc()로 생성

cq.select(m)
	.where(agetGt)
    .orderBy(cb.desc(m.get("age")));

(6) 조인

조인 join() 메소드와 JoinType 클래스를 사용한다.

public enum JoinType {
	INNER,
    LEFT,
    RIGHT
}

예)

Join<Member, Team> t = m.join("team", JoinType.INNER); //내부조인
  • 외부조인의 경우 JoinType.LEFT로 설정
  • FETCH JOIN의 경우 m.fetch("team", JoinType.LEFT);로 사용.

(7) 서브 쿼리

간단한 서브 쿼리

메인 쿼리와 서브 쿼리 간의 관련이 없는 단순한 서브쿼리

CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);

//서브쿼리
Subquery<Double> subQuery = mainQuery.subquery(Double.class);
Root<Member> m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.<Integer>get("age")));

상호 관련 서브 쿼리

메인 쿼리와 서브 쿼리 간의 서로 관련이 있을 때의 서브 쿼리는 서브쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야함. 서브 쿼리는 메인 쿼리의 Root나 Join을 통해 생성된 별칭을 받아 사용한다.

.where(cb.equal(subM.get("username"), m.get("username")));

(8) IN 식

IN식은 Criteria 빌더에서 in {...} 메소드를 사용함

CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);

cq.select (m)
  .where(cb.in(m.get("username"))
  .value("회원1")
  .value("회원2"));

(9) CASE 식

CASE 식에는 selectCase() 메소드와 when(), otherwise() 메소드를 사용함

Root<Member> m = cq.from(Member.class);

cq.multiselect(
	m.get("username"),
    cb.selectCase()
    	.when(cb.ge(m.<Integer>get("age"), 60), 600)
        .when(cb.le(m.<Integer>get("age"), 15), 500)
        .otherwise(100) );

(10) 파라미터 정의

JPQL에서 :PARAM1처럼 파라미터를 정의했듯이 Criteria도 파라미터를 정의할 수 있음.

CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);

//정의
cq.select(m)
	.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));
    
List<Member> resultList = em.createQuery(cq)
	.setParameter("usernameParam", "회원1") //바인딩
    .getResultList();
  • cb.parameter(타입, 파라미터 이름) 메소드를 사용해 파라미터를 정의
  • setParameter("usernameParam", "회원1")을 사용해 파라미터에 사용할 값을 바인딩.

(11) 네이티브 함수 호출

네이티브 SQL 함수 호출을 위해서는 cb.function() 메소드를 사용하면 됨.

Root<Member> m = cq.from(Member.class);
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);

(12) 동적 쿼리

동적 쿼리: 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 말함.

동적 쿼리는 문자 기반인 JPQL보다는 코드 기반인 Criteria로 작성하는 것이 더 편리함.

Criteria로 동적 쿼리를 구성하면 최소 공백이나, where, and의 위치로 인한 에러가 발생하지 않기에 동적 쿼리 한정 Criteria 작성이 편리할 수 있다. 다만, 장황하고 복잡하다는 단점은 여전히 존재한다.

(13) 함수 정리

Criteria는 JPQL의 빌더 역할을 하기에 JPQL 함수를 코드로 지원한다.
대부분은 CriteriaBuilder에 정의되어 있다.

조건함수: and(), or(), not(), equal(), notEqual(), lt() = lessThan(), bewteen(), isTrue(), in(), not(in()) 등

스칼라와 기타 함수: sum(), prod(), neg(), some(), any() 등

집합 함수: avg(), max(), min(), count() 등

(14) Criteria 메타 모델 API

Criteria는 코드 기반이기에 컴파일 시점에 오류를 발견할 수 있다. 다만 m.get("age")라는 코드를 작성 할 때 'age'는 문자이기에 이런 부분을 잘못 적는 것은 컴파일 시점에 에러를 발견하지 못한다.

이런 부분까지 코드로 작성하기 위해 메타 모델 API를 사용한다.

cq.select(m)
	.where(cb.gt(m.get(Member_.age), 20))
    .orderBy(cb.desc(m.get(Member_.age)));

다만 위를 적용하기 위해서는 메타 모델 클래스가 필요하다.

@GeneratedValue(value = "org.hibernate.jpmodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Member.class)
public abstract class Member {
	public static volatile SingularAttribute<Member, Long> id;
    public static volatile SingularAttribute<Member, String> username;
    public static volatile SingularAttribute<Member, Integer> age;
    public static volatile SingularAttribute<Member,Order> orders;
    public static volatile SingularAttribute<Member,Team> team;
}

위의 클래스를 표준(CANONICAL) 메타 모델 클래스라 하며 줄여서 메타 모델이라 한다.

Member_ 메타 모델 클래스는 Member 엔티티를 기반으로 만들어야한다. 다만, 개발자가 직접 이런 복잡한 코드를 작성하지는 않고, 코드 자동 생성기가 엔티티 클래스를 기반으로 메타 모델 클래스들을 만들어준다.

코드 생성기 설정

코드 생성기는 보통 Maven, Gradle 같은 빌드 도구들을 사용해서 실행한다

profile
이불 밖은 위험해.

0개의 댓글