10장. 객체지향쿼리언어(1) - JPQL 기초

배유연·2023년 11월 17일

JPA 프로그래밍 정리

목록 보기
11/11

1. 객체지향 쿼리 소개

JPQL 특징

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

JPA가 제공하는 다양한 검색방법

  • JPQL
  • Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 작성할 수 있다.
  • QueryDSL : 비표준 오픈소스 프레임워크, JPQL을 편하게 작성하도록 도와주는 클래스 모음
  • JDBC직접 사용, Mybatis 같은 SQL 매퍼 프레임워크 사용 : 필요하다면 JDBC직접 사용

❓JPQL 소개

JPQL 은 엔티티 객체를 조회하는 객체지향 쿼리다. 또한 JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL코드가 간결하다.

@Entity(name = "Member")
public class Member { 
	@Column(name = "name")
    private String username;
    
    //...
}
...
String jpql = "select m from Member as m wher m.username='kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();

select m from Member as m wher m.username='kim' 에서 Member는 엔티티 이름이고, m.username은 엔티티 객체의 필드명이다.

em.createQuery 메소드에 실행할 JPQL과 엔티티 클래스 타입을 넘겨주고 getResultList를 호출하면 JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회한다.

❓Criteria 쿼리 소개

Criteria 는 JPQL 을 생성하는 빌더 클래스다. Criteria 의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
즉, 컴파일 시점에 오류를 확인할 수 있다.

간단한 jpql 문이다.
select m from Member as m where m.username='kim'
이것을 Criteria 코드로 변환해 보자.

// 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.equals(m.get("username"),"kim"));

List<Member> resultList = em.createQuery(cq).getResultList();

위 코드에서 아쉬운 점은 m.get("username")으로 필드명을 문자로 작성한 점이다.
이 부분은 메타모델을 사용하면 코드로 작성할 수 있다.

//메타 모델 사용 전 -> 사용 후
m.get("username") -> m.get(Member_.username)

Criteria 가 장점은 많지만 장점을 상쇄할 정도로 복잡하다.. 한눈에 들어오지 않다.

❓QueryDSL 소개

QueryDSL도 Criteria 처럼 JPQL 빌더 역할을 한다. 좀 더 단순하고 한눈에 들어온다.

QueryDSL은 JPA표준은 아니고 오픈소스 프로젝트다.

//준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

//쿼리, 결과조회
List<Member> members = query.from(member)
					.where(member.username.eq("kim"))
					.list(member);

❓네이티브 SQL소개

JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데, 이것을 네이티브 SQL이라 한다.
단점은, 특정 데이터베이스에 의존하는 SQL을 작성해야한다는 것이다.

String sql = "SELECT ID, NAME FROM MEMBER WHERE NAME='kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

❓JDBC 직접사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용

JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야한다.
예)
1. JPA에서 영속성 컨텍스트에서 상품가격을 10,000원->9,000원 으로 변경(아직 플러시 하지않음.)
2. JDBC에서 직접 상품 조회 -> 10,000원으로 조회됨.

그러므로 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.


2. JPQL

시작하기 전에 이번 절에서 예제로 사용할 도메인 모델을 살펴보자.

UML

ERD

📌JPQL 기본 문법

SELECT문

SELECT m FROM Member AS m where m.username = 'HELLO'

  • 엔티티와 속성은 대소문자를 구분한다. 반면에 SELECT, FROM AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • JPQL 에서 사용한 Member 는 클래스명이 아니라 엔티티 명이다.
    @Entity(name="XXX")로 지정할 수 있다.
  • JPQL은 별칭을 필수로 사용해야한다.

    참고로 HQL을 사용하면 별칭없이 사용할 수 있다.

TypedQuery, Query

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

TypedQuery : 반환할 타입을 명확하게 지정할 수 있을 때 사용
Query : 반환타입을 명확하게 지정할 수 없을 때 사용.

TypedQuery<Member> query = em.createQuery("SELECT m from Member m", Member.class);
List<Member> resultList = query.getResultList();

em.createQuery에 두번째 파라미터를 지정하면 ? TypedQuery 반환
지정안하면 Query 반환으로 동작한다.

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

위 코드를 보면, 조회 대상이 String 타입인 회원이름과 Integer 타입인 나이이므로 조회 대상 타입이 명확하지 않다. 따라서 Query 객체를 사용해야한다.

결과 조회

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

📌파라미터 바인딩

JDBC 는 위치 기준 파라미터 바인딩만 지원하지만, JPQL은 이름 기준 파라미터 바인딩도 지원한다.

이름기준 파라미터

:를 사용한다.

TypedQuery<Member> query = em.createQuery(
				"SELECT m from Member m where m.username = :username", Member.class);
query.setParameter("username" , usernameParam);
List<Member> result = query.getResultList();

참고로 JPQL API는 대부분 메소드 체인 방식으로 설계되어 있다.

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

이렇게 연속해서 작성이 가능하다.

위치 기준 파라미터

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

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

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

파라미터 바인딩 방식을 무조건 사용하자!
파라미터 바인딩 방식을 사용하면 SQL Injection 공격을 막을 수 있고, 또한 파라미터 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있다. 그리고 데이터베이스도 내부에서 실행한 SQL을 파싱해서 사용하는데, 같은 쿼리는 파싱한 결과를 재사용할 수 있다.

📌프로젝션

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하고 [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.
프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.
스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.

엔티티 프로젝션

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

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

임베디드 타입 프로젝션

JPQL 에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
예를 들어서...

String query = "SELECT a FROM Address a";

위 코드는 잘못됐다. Address 타입은 임베디드 타입이어서 시작이 될 수 없다.

다음 코드는 올바른 코드이다.

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

위 코드를 실행하면 아래 SQL이 실행된다.

SELECT 
	CITY
    STREET
    ZIPCODE
FROM
	ORDERS 

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

스칼라 타입 프로젝션

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

SELECT DISTINCT username FROM Member m 

여러 값 조회

프로젝션에 스칼라 타입의 여러 값을 선택하려면 TypedQuery 를 사용할 수 없고 대신에 Query 를 사용해야한다.

Query query = em.createQuery("SELET 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];
}

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

Query query = em.createQuery("SELET o.member, o.product, o.orderAmount FROM Order m");
List<Object[]> resultList = query.getResultList();

for(Object[] row : resultList)
	Member member = (Member) row[0];	//엔티티
    Product member = (Product) row[1];	//엔티티
    int member = (Integer) row[2];		//스칼라
}

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

NEW 명령어

NEW를 사용하기 전 객체 변환작업을 코드를 통해 확인해보자.

Query query = em.createQuery("SELET m.username, m.age FROM Member m");
List<Object[]> resultList = query.getResultList();

//객체 변환 작업
List<UserDTO> userDTOs = new ArrayLists<UserDTO>();
for(Object[] row : resultList){
	UserDTO userDTO = new UserDto((String)row[0], (Integer)row[1]);
    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();

SELECT 다음에 new 명령어를 사용하면 반환받을 클래스를 지정할 수 있다.

new 명령어를 사용할 때 주의사항

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

📌 페이징 API

JPA는 페이징을 두 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작한다.)
  • setMaxResults(int maxResult) : 조회할 데이터 수
private static void paging(EntityManager em) {
        TypedQuery<Member10_1> query = em.createQuery(
                "SELECT m FROM Member10_1 m ORDER BY m.username DESC", Member10_1.class);
        query.setFirstResult(10);
        query.setMaxResults(20);
        List<Member10_1> resultList = query.getResultList();
    }

데이터베이스 마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. 나는 MYSQL을 사용하여서 아래 쿼리가 실행되었다.

select
	member10_1x0_.memberId as memberId1_19_,
    member10_1x0_.name as name2_19_ 
from
	Member10_1 member10_1x0_ 
order by
    member10_1x0_.name DESC limit ?,?

📌 집합과 정렬

집합함수

SELECT
	COUNT(m), //회원수
    SUM(m.age), //나이합
    AVG(m.age), //평균나이
    MAX(m.age), //최대 나이
    MIN(m.age) // 최소 나이
FROM Member m

집합 함수 사용시 참고사항

  • NULL 값은 무시하므로 통계에 잡히지 않는다.
  • 값이 없을 대 SUM, AVG, MAX, MIN을 사용하면 NULL값이 된다.
    단, COUNT는 0이다.
  • DISTINCT 를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
    ex) select COUNT(DISCOUNT m.age) from Member m
  • DISTINCT 를 COUNT 에서 사용할 때 임베디드 타입은 지원하지 않는다.

GROUP BY, HAVING

GROUP BY 는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
HAVING 은 GROUP BY 와 함께 사용하면서, 그룹화한 통계 데이터를 기준으로 필터링한다.

SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AGE(m.age) >= 10

정렬

ORDER BY 는 결과를 정렬할 때 사용한다.

  • ASC : 오름차순(기본값)
  • DESC : 내림차순

📌JPQL 조인

📍내부조인

내부조인은 INNER JOIN을 사용한다. 참고로 INNER는 생략할 수 있다.

SELECT m 
FROM Member m INNTER JOIN m.tream t 
WHERE t.name= : teamName

해당 JPQL 실행시

select
            member10_2x0_.MEMBER_ID as MEMBER_I1_20_,
            member10_2x0_.TEAM_ID as TEAM_ID3_20_,
            member10_2x0_.username as username2_20_ 
        from
            Member10_2_7 member10_2x0_ 
        inner join
            Team10_2_7 team10_2_7x1_ 
                on member10_2x0_.TEAM_ID=team10_2_7x1_.TEAM_ID 
        where
            team10_2_7x1_.name=?
Hibernate: 
    select
        team10_2_7x0_.TEAM_ID as TEAM_ID1_47_0_,
        team10_2_7x0_.name as name2_47_0_ 
    from
        Team10_2_7 team10_2_7x0_ 
    where
        team10_2_7x0_.TEAM_ID=?

inner join 은 되지만, N+1 현상이 발생한다. N+1 현상은 뒤에서 더 자세히 알아보자.
JPQL 은 JOIN 명령어 다음에 조인할 객체의 연관필드를 사용한다.

📍외부조인

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

OUTER 는 생략가능하다.
위 쿼리 실행시

select
            member10_2x0_.MEMBER_ID as MEMBER_I1_20_,
            member10_2x0_.TEAM_ID as TEAM_ID3_20_,
            member10_2x0_.username as username2_20_ 
        from
            Member10_2_7 member10_2x0_ 
        left outer join
            Team10_2_7 team10_2_7x1_ 
                on member10_2x0_.TEAM_ID=team10_2_7x1_.TEAM_ID
Hibernate: 
    select
        team10_2_7x0_.TEAM_ID as TEAM_ID1_47_0_,
        team10_2_7x0_.name as name2_47_0_ 
    from
        Team10_2_7 team10_2_7x0_ 
    where
        team10_2_7x0_.TEAM_ID=?

역시나 N+1 현상 발생한다.

📍컬렉션 조인

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

  • 회원->팀 으로의 조인은 다대일 조인이면서 단일값 연관필드를 사용한다.
  • 팀->회원 은 반대로 일대다 조인이면서 컬렉션 값 연관필드(t.mebers)를 사용한다.

이렇게 사용하면 된다.
select t, m from Team t left join t.members m

📍세타 조인

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

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

위 jpql 실행시

	select
            count(member10_2x0_.id)
        from
            Member10_2_7 member10_2x0_ 
        cross join
            Team10_2_7 team10_2_7x1_ 
        where
            member10_2x0_.username=team10_2_7x1_.name

쿼리가 실행된다.

📍JOIN ON 절 (JPA 2.1)

JPA 2.1 부터 조인할 때 ON 절을 지원한다. ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
참고로 내부조인의 ON절은 WHERE절을 사용할 때와 결과가 같으므로 보통 ON절은 외부조인에서만 사용한다.

private static void useJoinOn(EntityManager em) {
        String query = "SELECT m FROM Member10_2_7 m left join m.team t on t.name='팀A'";

        List<Member10_2_7> members = em.createQuery(query, Member10_2_7.class)
                .getResultList();

        System.out.println("members = " + members);
    }

수행하면 아래 sql이 수행된다.

    select
           member10_2x0_.MEMBER_ID as MEMBER_I1_20_,
           member10_2x0_.TEAM_ID as TEAM_ID3_20_,
           member10_2x0_.username as username2_20_ 
        from
            Member10_2_7 member10_2x0_ 
        left outer join
            Team10_2_7 team10_2_7x1_ 
                on member10_2x0_.TEAM_ID=team10_2_7x1_.TEAM_ID 
                and (
                    team10_2_7x1_.name='팀A'
                )
Hibernate: 
    select
        team10_2_7x0_.TEAM_ID as TEAM_ID1_47_0_,
        team10_2_7x0_.name as name2_47_0_ 
    from
        Team10_2_7 team10_2_7x0_ 
    where
        team10_2_7x0_.TEAM_ID=?

위 쿼리를 보면, 조인시점에 조인대상을 필터링 한다. 그런데 또 N+1현상 발생!


📌페치 조인

페치조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데, join fetch 명령어로 사용할 수 있다.

🧷엔티티 페치 조인

페치조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회해보자.
select m from Member m join fetch m.team
수행시

	select
            member10_2x0_.MEMBER_ID as MEMBER_I1_20_0_,
            team10_2_7x1_.TEAM_ID as TEAM_ID1_47_1_,
            member10_2x0_.TEAM_ID as TEAM_ID3_20_0_,
            member10_2x0_.username as username2_20_0_,
            team10_2_7x1_.name as name2_47_1_ 
        from
            Member10_2_7 member10_2x0_ 
        inner join
            Team10_2_7 team10_2_7x1_ 
                on member10_2x0_.TEAM_ID=team10_2_7x1_.TEAM_ID

N+1 현상 발생하지 않는다.

참고로, JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.
(그렇지만 JPA구현체인 하이버네이트는 별칭을 허용한다.)
자세한건 여기 참고!

그리고 엔티티 페치조인 JPQL에서 select m 으로 회원엔티티만 선택했는데, 실행된 SQL을 보면 회원과 팀도 함께 조회된 것을 확인할 수 있다.

만약 회원과 팀을 지연로딩 설정했다면?
아무리 지연로딩으로 설정해도 페치조인을 설정해두어서, 실제 엔티티가 조회되고, 지연로딩이 일어나지 않는다.
또한 회원엔티티가 영속성 컨텍스트에서 분리되어 준영속상태가 되어도 연관된 팀을 조회할 수 있다.

🧷컬렉션 페치 조인

일대다 관계인 컬렉션을 페치 조인해보자.

	String query = "select t from Team10_2_7 t join fetch t.members where t.name='팀A'";

    List<Team10_2_7> teams = em.createQuery(query, Team10_2_7.class)
                .getResultList();

    for (Team10_2_7 team : teams) {
    	System.out.println("team = " + team);
        List<Member10_2_7> members = team.getMembers();
        System.out.println("members = " + members);
        System.out.println("------------------------------------");
	}
	

이렇게 하면 연관된 회원 컬렉션도 함께 조회한다.

	select
            team10_2_7x0_.TEAM_ID as TEAM_ID1_47_0_,
            members1_.MEMBER_ID as MEMBER_I1_20_1_,
            team10_2_7x0_.name as name2_47_0_,
            members1_.TEAM_ID as TEAM_ID3_20_1_,
            members1_.username as username2_20_1_,
            members1_.TEAM_ID as TEAM_ID3_20_0__,
            members1_.MEMBER_ID as MEMBER_I1_20_0__ 
        from
            Team10_2_7 team10_2_7x0_ 
        inner join
            Member10_2_7 members1_ 
                on team10_2_7x0_.TEAM_ID=members1_.TEAM_ID 
        where
            team10_2_7x0_.name='팀A'
            
team = Team10_2_7{id='team1', name='팀A'}
members = [Member10_2_7{id='member1', username='멤버일', team=Team10_2_7{id='team1', name='팀A'}}
, Member10_2_7{id='member4', username='멤버사', team=Team10_2_7{id='team1', name='팀A'}}]
------------------------------------
team = Team10_2_7{id='team1', name='팀A'}
members = [Member10_2_7{id='member1', username='멤버일', team=Team10_2_7{id='team1', name='팀A'}}
, Member10_2_7{id='member4', username='멤버사', team=Team10_2_7{id='team1', name='팀A'}}]

페치조인으로 지연로딩이 발생하지 않고, '팀A' 두 건 조회된 것을 확인할 수 있다.

만약 동일한 것은 한 건만 조회하고 싶을때는?
DISTINCT를 이용하면 중복제거가 가능하다.
select distinct t from Team10_2_7 t join fetch t.members where t.name='팀A'

🧷페치조인과 일반 조인의 차이 - 어디까지 조회할 것인가.

일반조인문을 보자.

String query3 = "SELECT t FROM Team10_2_7 t inner join t.members m";

List<Team10_2_7> teams = em.createQuery(query3, Team10_2_7.class).getResultList();

위 코드를 실행하면

	select
            team10_2_7x0_.TEAM_ID as TEAM_ID1_47_,
            team10_2_7x0_.name as name2_47_ 
        from
            Team10_2_7 team10_2_7x0_ 
        inner join
            Member10_2_7 members1_ 
                on team10_2_7x0_.TEAM_ID=members1_.TEAM_ID

페치조인과 달리, 회원은 전혀 조회되지 않는다.
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
따라서 연관된 회원 컬렉션은 지연로딩으로 DEFAULT설정 되어있어서 회원을 조회하기 위한 쿼리가 더 실행된다.

🧷페치조인의 특징

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

글로벌 로딩 전략이란?
@OneToMany(fetch = FetchType.LAZY)
처럼 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부른다.

페치 조인은 글로벌 로딩 전략보다 우선한다.
아무리 지연로딩을 설정해도 페치조인을 사용하면 한꺼번에 조회한다.
따라서 글로벌 로딩 전략은 될 수 있으면 지연로딩을 설정하고, 최적화가 필요하면 페치조인을 적용하는 것이 효과적이다.

🧷페치조인의 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.(하이버네이트는 사용할 수 있게 해두었다..) 자세한 내용은 해당링크로!

  • 둘 이상의 컬렉션을 페치할 수 없다.
    - 구현체에 따라 되긴하지만.. 컬렉션*컬렉션의 카테시안 곱이 만들어지므로 주의해야함.

  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    - 컬렉션이 아닌, 단일값 연관필드(일대일, 다대일)은 페치조인 해도 페이징 api사용가능


📌경로 표현식

경로표현식이란 쉽게 말해서, .을 찍어 객체 그래프를 탐색하는 것이다.

🧷경로표현식 용어

  • 상태 필드 : 단순히 값을 저장하기 위한 필드
    - 경로탐색의 끝이다.
  • 연관필드 : 연관관계를 위한 필드, 임베디드 타입 포함
    -단일 값 연관필드
    : 묵시적으로 내부 조인이 일어난다. 계속적인 탐색이 가능
    -컬렉션 값 연관필드
    : 묵시적으로 내부조인이 일어난다. 더는 탐색이 불가능하다.
    단, join을 통해 별칭을 얻으면 탐색가능

코드를 통해 용어정리를 해보겠다.

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    private String username; //상태필드

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //연관필드(단일값 연관 필드)
    
    @OneToMany(mappedBy = "member")
    private List<Order> orders ; //연관 필드(컬렉션 값 연관필드)
    

🧷상태 필드 경로 탐색

select m.username, m.age from Member m
m.usernamem.age 가 상태필드 경로탐색이다.

🧷단일 값 연관 경로 탐색

select o.member from Order o
실제 실행되는 SQL은 어떻게 될까?

select m.*
from Orders o
inner join Member m on o.member_id = m.id

단일값 연관필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데, 이것을 묵시적 조인이라 한다.
참고로 묵시적 조회는 모두 내부조인이다.

🧷컬렉션 값 연관 경로 탐색

select t.members.username from Team t
이렇게 사용하면 오류가 난다. t.members까지는 탐색 가능하지만, 컬렉션에서 경로탐색을 시작하는 것은 허락하지 않는다.
만약에 탐색을 하고 싶다면?
select m.username from Team t join t.members m
이렇게 새로운 별칭을 얻어서 사용하면 된다.

컬렉션의 크기를 구할 수 있는, size라는 기능?
select t.members.size from Team t
이렇게 size를 사용하면 COUNT 함수를 사용하는 SQL로 바뀐다.

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

  • 항상 내부조인이다.
  • 컬렉션은 경로 탐색의 끝이다.
  • 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자.

📌서브 쿼리

JPQL도 SQL 처럼 서브쿼리를 지원한다.
차이는? WHERE, HAVING절에만 사용가능하고, SELECT/FROM 절에서는 사용할 수 없다.

🧷서브 쿼리 함수

  • [NOT] EXISTS (subquery)
  • {ALL | ANY | SOME} (subquery)
  • {NOT} IN (subquery)

📌조건식

JPQL에서 사용하는 조건식을 보자.

🧷타입표현

대소문자를 구분하지 않는다.

날짜표현
Date : {d '2023-11-16'}
Time : {t '10-11-11'}
DATETIME : {ts '2023-11-16 16-11-10.123'}

문자표현
작은 따옴표 사이에 표현한다. 작은따옴표를 표현하고 싶으면 작은 따옴표 연속 두개를 사용한다.

숫자표현
10L : 롱타입 지정
10D : 더블타입 지정
10F : 플롯타입 지정

🧷컬렉션 식

컬렉션식은 컬렉션에만 사용하는 특별한 기능이다. 참고로 컬렉션은 컬렉션 식 이외에 다른 식을 사용할 수 없다.

빈 컬렉션 비교식
IS [NOT] EMPTY : 컬렉션에 값이 비었으면 참
ex)
select m from Member m where m.orders is not empty
위 jpql을 실행하면 exists 명령어로 변경된다.
주의점은 컬렉션이 비었는지 확인하기 위해 is null을 사용할 수 없다는 것이다.

컬렉션의 멤버식
{엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관경로}
: 엔티티나 값이 컬렉션에 포함되어 있으면 참
ex)
select t from Team t where :memberParam member of t.members

📌다형성쿼리

JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
예를 들어보자. Item의 자식으로 Book, Album, Movie가 있다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="DTYPE")
public abstract class Item{...}

@Entity
@DiscriminatorValue("B")
public class Book extends Item { 
	...
    private String author;
}
//...Album, Movile 엔티티도 생성

이렇게 엔티티가 상속관계에 있을 때,
select i from Item i
해당 jpql을 실행하면 어떻게 될까?

단일 테이블 전략일 경우
SELECT * FROM ITEM

조인 전략일 경우

SELECT i.ITEM_ID, i.DTYPE...,
	   b.author, ...,
       a.artist, ..,
       m.actor, ...
FROM 
	ITEM i
LFET OUTER JOIN
	BOOK b on i.ITEM_ID = b.ITEM_ID
LFET OUTER JOIN
	ALBUM a on i.ITEM_ID = a.ITEM_ID
LFET OUTER JOIN
	MOVIE m on i.ITEM_ID = m.ITEM_ID

🧷 TYPE이란?

TYPE이란 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용한다.
ex) Item 중에 Book, Movie를 조회하라.

jpql
select i from Item i where type(i) In(Book, Movie)

sql 변환쿼리
select i.* from ITEM i where i.DTYPE in ('B', 'M')

🧷 TREAT?

TREAT는 JPA 2.1에 추가된 기능인데, 자바의 타입 캐스팅과 비슷하다.
상속구조에서 부모타입을 특정 자식타입으로 다룰 때 사용한다.
JPA 표준은 FROM, WHER절만 사용가능하고, 하이버네이트에서는 SELECT에서도 사용가능하다.

JPQL
select i from Item i where treat(i as Book).author = 'kim'

sql 변환쿼리
select i.* from Item i where i.DTYPE='B' and i.author = 'kim'


📌사용자 정의 함수 호출

JPA 2.1 부터 사용자 정의 함수를 지원한다.
ex) select function('test_func', i.name) from Item i
이 함수를 사용하기 위해서는 하이버네이트에서는 방언클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야한다.

public class MyH2Dialect extends H2Dialect {
	public MyH2Dialect(){
    	registerFunction("test_func", new StandardSQLFunction("test_func", StandardBasicTypes.STRING));
    }
}

그리고 hibernate.dialect에 해당 방언을 등록해야한다.

persistence.xml

<property name="hibernate.dialect" value="hello.MyH2Dialect"/>

하이버네이트 구현체를 사용하면 축약해서 함수를 호출할 수 있다.
select test_func(i.name) from Item i


📌기타정의

  • enum은 비교 연산만 지원한다.
  • 임베디드 타입은 비교를 지원하지 않는다.

EMPTY STRING
JPA 표준은 ''을 길이 0인 Empty String으로 정했지만 데이터베이스에 따라 ''을 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용해야 한다.

📌엔티티 직접 사용

기본키 값
JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
ex)

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

위 jpql를 실행하면 동일한 sql이 실행된다. 즉, 엔티티를 직접사용해도 SQL로 변환될 때 해당 엔티티의 기본키를 사용한다는 말이다.

외래키 값
외래키도 마찬가지로 엔티티를 참조하던, 외래키값을 참조하던 실제 실행되는 SQL에서는 외래키를 사용해서 실행된다.

📌Named 쿼리 : 정적 쿼리

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

Named 쿼리
Named 쿼리는 애플리 케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둔다.
따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에 파싱된 결과를 재사용하여 성능상 이점도 있다.
Named쿼리는 @NamedQuery 어노테ㅣ션을 사용해서 자바 코드에 작성하거나, XML문서로 작성할 수 있다.

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

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

...function(){
	List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    		.setParameter("username","회원1")
            .getResultList();
}

위처럼 @NamedQuery 어노테이션을 사용하여 정의하면 된다.
name을 보면 Member.findByUsername 이라고 엔티티이름을 주었는데, 기능적으로 특별한 의미는 없다. 그렇지만 영속성 유닛단위로 Named쿼리가 관리되므로 충돌방지를 위해 넣는 것이 좋고, 관리하기 편해진다.

2개 이상의 Named 쿼리를 정의하려면 @NamedQuerries 어노테이션을 사용하면 된다.

Named쿼리를 XML에 정의
JPA에서 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.
META-INF/orm.xml은 JPA가 기본 매핑파일로 인식해서 별도의 설정을 하지 않아도되지만 커스텀해서 사용할 거면, persistence.xml에 추가 설정을 해야한다.
persistence.xml

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

ormMember.xml

<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
  version="2.1">
    <named-query name="Member.findByUsername">
        <query><![CDATA[
            select m from Member m where m.username = :username
            ]]></query>
    </named-query>

</entity-mappings>

<![CDATA[]]> 를 사용하면 그 사이에 문장을 그대로 출력하므로 예약문자도 사용할 수 있다.

만약 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.

profile
기초를 중요시하는 백엔드개발자입니당

0개의 댓글