코딩은 마라톤

[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (1) 본문

Backend/JPA

[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (1)

anxi 2024. 6. 16. 23:57

JPQL 소개

  • JPQL(Java Persistence Query Language)은 엔티티 객체를 조회하는 객체지향 쿼리이다.
    • 테이블을 대상으로 쿼리하지 않고 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
  • JPQL은 SQL보다 간결하다.

기본 문법과 쿼리 API

JPQL은 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있지만 INSERT는 없다.

(엔티티 저장 시 EntityManager.persist() 메서드 사용하기 때문이다.)

 

SELECT 문

예시

SELECT m FROM Member AS m where m.username = ’Hello'

 

  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분하지만, SELECT, FROM과 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    • 엔티티 명은 @Entity(name="xxx") 로 지정한 값을 사용하거나, 클래스 명을 기본으로 사용한다.
  • 별칭은 필수
    • Member AS m에서 Member에 m이라는 별칭을 준다. 
    • JPQL은 별칭을 필수로 사용한다.
    • AS는 생략 가능하다.

TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만든다.

  • 쿼리 객체의 반환 타입이 명확하면 TypeQuery
  • 명확하지 않음 Query를 사용한다.
  • TypeQuery 사용
TypedQuery<Member> query = em.createQuery ("SELECT m FROM Member m" , Member.class);
List<Member> resultList = query.getResultList();
for (Member member : resultList) {
    System.out.printIn("member = " + member);
}

 

  • Query 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for (Object o : resultList) {
    //결과가 둘 이상이면 Object [] 반환
    Object[] result = (Object []) o;
    System.out.printin("username = " + result[0]);
    System.out.printin("age = " + result[1]);
}

 

결과 조회

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

파라미터 바인딩

  • 이름 기준 파라미터 (:변수명)
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
query.setParameter("username", usernameParam);

// 또는 메서드 체인을 사용한다.
query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
    .setParameter("username”, usernameParam);

 

  • 위치 기준 파라미터 (?위치값)
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
    .setParameter(1, usernameParam)
    ...

 

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

 

프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다. [SELECT {프로젝션 대상} FROM]
  • 프로젝션 대상은 엔티티, 엠비디드, 스칼라(숫자, 문자 등 기본 데이터) 타입이 있다.
  • 엔티티 프로젝션
SELECT m.team FROM Member m;
  • 임베디드 타입 프로젝션

엔티티와 거의 비슷하게 사용한다. 하지만 임베디드 타입은 조회의 시작점에서 사용할 수 없다.

// Address는 임베디드 타입이라 바로 조회 X
String query = "SELECT a FROM Address a"; 

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

 

임베디드 타입은 엔티티 타입이 아닌 값 타입이라 영속성 컨텍스트에서 관리되지 않는다.

  • 스칼라 타입 프로젝션
Double orderAmountAvg = em.createQuery(”SELECT AVG(o.orderAmount) FROM Order o”, Double.class).getSingleResult();

 

  • 여러 값 조회

만약 필요한 데이터들만 선택해서 조회할 때는 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.

List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o").getResultList();
for (Object[] row : resultList) {
    Member member = (Member) row[0]; // 엔티티
    Product product = (Product) row[1] ; // 엔티티
    int orderAmount = (Integer) row[2] ; // 스칼라
}
  • 객체 변환 => NEW 명령어 사용

만약 username과 age 두 필드를 프로젝션하기 위해서 Object[]를 반환하여 사용할 것이다.

하지만 실제 애플리케이션 개발시 Object[] 보단 DTO 객체와 같이 의미있는 객체로 변환하여 사용한다.

List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m").getResultList() ;
// 객체 변환 작업
List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for (Object[] row : resultList) {
    UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[l]);
    userDTOs.add(userDTO);
}
return userDTOs;

 

상기 코드는 직접 변환하는 과정이다.

하지만 위와 같은 필드가 많아지거나 다른 변수에 있어 객체 변환을 직접 하는 것은 지루하다.

따라서 하기 코드와 같이 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();

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

 

NEW 명령어를 사용하면 TypeQuery를 사용할 수 있다.

 

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

페이징 API

JPA 페이징을 다음 API 추상화했다

  • setFirstResult (int startPosition) : 조회 시작 위치(0부터 시작한다) == pageNumer
  • setMaxResults (int maxResult) : 조회할 데이터 수 == pageSize
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();

 

조회 위치는 11부터, 데이터 수는 20건의 데이터를 조회한다.

따라서 11번부터 30번까지의 데이터를 조회한다.


JPQL 조인

내부 조인

내부 조인은 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에 소속된 회원을 조회하는 쿼리다.

생성된 내부 조인 SQL은 다음과 같다.

SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM
    MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
    T.NAME=?

 

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다. (Team <-> Member)

연관필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드이다.

 

만약 다른 타입의 두 엔티티를 조회했을 경우 TypeQuery를 사용할 수 없다.

String query = "SELECT m, t FROM Member m JOIN m.team t";
List<Object[]> result = em.createQuery(query).getResultList();

for (Object[] row : result) {
    Member member = (Member) row[0];
    Team team = (Team) row[1];
)

외부 조인

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

 

위에서 LEFT 조인을 사용하면 다음 SQL이 실행된다.

SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM
    MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
    T.NAME=?

 

컬렉션 조인

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

  • [Member -> Team] 으로의 조인은 다대일 조인이면서 "단일 값 연관 필드(m.team)"를 사용한다.
  • [Team -> Member] 은 반대로 일대다 조인이면서 "컬렉션 값 연관 필드(t.members)"를 사용한다.
// 컬렉션 값 연관 필드 외부 조인
SELECT t, m FROM Team t LEFT JOIN t.members m

세타 조인

  • WHERE 절을 사용해서 세타 조인을 할 수 있다.
  • 세타 조인은 내부 조인만 지원한다.
  • 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.

세타조인은 특정 조건을 만족하는 테이블의 로우들을 조합하는 조인 방식입니다. 이는 ‘=’ 외에도 ‘<’, ‘>’, ‘!=’ 같은 다양한 비교 연산자를 사용할 수 있는 조인의 일종이며, 동등 조인(Equi-Join)은 ‘=’ 연산자를 사용하는 세타조인의 한 예입니다. 세타조인은 보통 ON 절을 사용해 조인 조건을 명시합니다.

 

즉, 두 릴레이션(엔티티, 테이블)에서 공통된 attribute를 기준으로 조건을 만족하는 튜플(행)들을 결합하는 것이다.

 

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

//SQL
SELECT COUNT(M.ID)
FROM
    MEMBER M CROSS JOIN TEAM T
WHERE
    M.USERNAME=T.NAME

페치 조인

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

엔티티 페치 조인

페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL은 다음과 같다.

select m from Member m join fetch m.team

 

연관된 엔티티나 컬렉션을 함께 조회한다. Member(m), Team(m.team)

페치 조인은 별칭을 사용할 수 없다.

 

SELECT
    M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

 

페치 조인 결과

 

SELECT 쿼리를 통해 회원 엔티티만 선택해서 조회해도 연관된 팀도 함께 조회된다.

만약 연관관계에서 fetch=FetchType.LAZY 와 같이 지연 로딩을 설정하더라도 페치 조인을 사용하면 팀도 함께 조회했으므로
팀 엔티티는 프록시가 아닌 실제 엔티티이다. 
따라서 연관된 팀을 사용해도 지연 로딩이 일어나지 않는다.

 

컬렉션 페치 조인

일대다 관계인 컬렉션을 페치 조인하면 다음과 같다.

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

 

실행된 SQL은 다음과 같다.

SELECT
    T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

페치 조인 결과 테이블
컬렉션 페치 조인 결과 객체

 

팀A는 하나지만 회원 테이블과 조인하면서 결과가 증가해서 위와 같이 팀A는 2건 조회된다.

특히, 일대다 조인에서 결과가 증가한다. (일대일, 다대일 조인은 결과가 증가하지 않는다.)

 

페치 조인과 DISTINCT

위와 같이 중복의 경우를 제거하기 위해 DISTINCT 명령어를 사용한다.

JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하고 애플리케이션에서 한 번 더 중복을 제거한다.

 

위에서 팀A의 중복은 각 로우의 데이터를 중복 제거하는 DISTINCT가 아닌 애플리케이션에서 중복된 데이터를 걸러낼 때 사용한다.

페치 조인 DISTINCT 결과

 

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

페치 조인을 사용하지 않고 일반 조인을 사용해보면 다음과 같다.

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

// 결과
SELECT
    T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = ’팀A’

 

일반 조인에서는 회원 컬렉션과 팀 컬렉션이 조인했다고 해서 두 결과를 같이 가져오지 않는다.

SELECT 절에서는 지정한 엔티티만 조회한다.

  • 회원 컬렉션 : EAGER
    • 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.
  • 회원 컬렉션 : LAZY
    • 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.

반면에 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.

 

페치 조인의 특징과 한계

특징

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

 

  • 글로벌 로딩 전략 : @0neToMany (fetch = FetchType. LAZY)와 같은 지연 로딩과 즉시 로딩을 엔티티에 직접 적용하는 것
  • 페치 조인은 글로벌 로딩 전략보다 우선하여 글로벌 로딩 전략을 지연 로딩으로 설정해도 페치 조인을 사용시 페치 조인을 적용해서 함께 조회한다.
따라서, 최적화를 위해 글로벌 로딩 전략은 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 사용하는 것이 효과적이다.

 

한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다. (일대다 연관관계만 해당)