어플리케이션개발/JPA

JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (1)

안잡아모찌 2024. 1. 29. 19:41

JPQL (Java Persistence Query Language)

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

//쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();

 * 기존의 SQL 과 다른 점은 테이블의 컬럼명이 아니라 엔티티 객체의 필드명이라는 것이다.

<JPQL>
select m
from Member as m
where m.username = 'kim'


<SQL>
select
	member.id as id,
    member.age as age,
    member.team_id as team,
from
	Member member
where member.name = 'kim'

 

이 외에, Criteria, QueryDSL, 네이티브 SQL, JDBC 직접 사용, SQL 매퍼 프레임워크 사용 등의 객체지향 언어쿼리도 존재한다.

 

기본 문법과 쿼리 API

JPQL은 SELECT, UPDATE, DELETE문을 사용할 수있다.

SELECT m FROM Member as m where m.username = 'Hello'
  • 대소문자 구분
    : 엔티티와 속성은 대소문자를 구분한다
  • 엔티티 이름
    : From 절에 Member는 클래스 명이 아니라 엔티티 명이다
  • 별칭은 필수
    : JPQL은 별칭을 필수도 사용해야 한다.

 

 

TypeQuery, Query

  • TypeQuery : 타입을 명확하게 지정할 수 있을때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = qeury.getResultList();

 

  • Query : 반환 타입을 명확하게 지정할 수 없을때 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();

for(Object o : resultList) {
	Object[] result = (Object[]) o; //결과가 둘 이상이면 Object[] 반환
}

해당 예제 처럼 조회 대상이 둘 이상이면 Object[]를 반환하고 하나면 Object를 반환한다.

 

결과 조회

  • query.getResultList() : 결과를 예제로 반환한다.
  • query.getSingleResult() : 결과가 정확히 하나일 때 사용한다. 아니면 에러 발생

 

파라미터 바인딩

  • 이름 기준 파라미터
String usernameParam = "User1";

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

:변수 로 기준 파라미터를 정의하고 setParameter함수를 사용하여 바운딩 해준다.

 

프로젝션

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다.

  • 엔티티 프로젝션
    SELECT m FROM member m 과 같이 엔티티를 프로젝션으로 갖는 것
    따라서, 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
  • 임베디드 타입 프로젝션
    임베티드 타입으로 정의한 값 타입을 조회하는 것
    따라서, 영속성 컨텍스트에서 관리되지 않는다.
  • 스칼라 타입 프로젝션
    숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다.
  • NEW 명령어
    새로운 DTO를 정의해서 의미 있는 객체로 변환해서 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserDTO {
    private String username;
    private int age;
    
    public UserDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
    //...
}
 
TypedQuery<UserDTO> query = 
    em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
    FROM Member m", UserDTO.class);
 
List<UserDTO> resultList = query.getResultList();
cs
  1. 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
  2. 순서와 타입이 일치하는 생성자가 필요하다

 

페이징 API

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

 

집합(COUNT, MAX, MIN, AVG, SUM, GROUP BY, HAVING), 정렬(ORDER BY)는 SQL과 동일

 

 


JPQL 조인

 

내부조인
1
2
3
4
5
6
7
String teamName = "TeamA";
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();
cs

SQL과 약간 다른 부분은 연관 필드를 사용한다는 것이다. 여기서 m.team이 연관 필드이다.

  • Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.

 

외부 조인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
 
 
[SQL]
SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM 
    MEMBER M LEFT JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE 
    T.NAME = ?
cs

sql과 기능상 같다.

 

컬렉션 조인

? : N (?대다) 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라고 한다.

SELECT t, m From Team t LEFT JOIN t.members m

팀이 보유한 회원 목록을 컬렉션 값 연관 필드로 외부 조인했다.

 

세타 조인

WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타조인은 내부 조인만 지원한다.

1
2
3
4
5
6
7
8
9
10
11
//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
cs

 

JOIN ON 절

보통 ON절은 외부 조인에서만 사용한다.

1
2
3
4
5
6
7
8
//JPQL
select m, t from Member m
left join m.team t on t.name = 'a'
 
 
//SQL
SELECT m.*, t.* FROM Member M
LEFT JOIN Team t ON m.TEAM_ID = t.id and t.name = 'A'
cs

 

페치 조인

JPQL에서 성능 최적화를 위해 제공하는 기능이며 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다.

join fetch 명령어로 사용할 수 있다.

 

엔티티 페치 조인

1
2
3
4
5
6
7
8
9
select m
from Member m join jetch m.team // m.team 에는 별칭을 사용할 수 없다.
 
 
[SQL]
SELECT
    M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
cs

해당 조인을 사용하면 회원에 연관된 팀도 한번에 조회된다.

1
2
3
4
5
6
7
8
9
String jpql = "select m from Member m join fetch m.team";
 
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
 
for(Member member : members) {
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안 함
    member.getUsername();
    member.getTeam().name();
}
cs

페치조인이 지연로딩보다 우선순위를 갖기때문에, Team의 정보를 사용할 때 지연로딩이 발생하지 않는다.

 

컬렉션 페치 조인

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


[SQL]
SELECT
	T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = 'teamA'

? : N 처럼 다 관계를 향한 컬렉션 페치 조인을 사용하게 되면, 팀에 대한 정보를 불러오더라도 회원에 개수만큼 더 많은 건수를 조회하게 된다.

그림과 같이 teamA가 두건이 조회되게 된다.

📌 이렇듯, 일대다 조인은 결과가 증가할 수 있지만 일대일, 다대일 조인은 결과가 증가하지 않는다.

 

페치 조인의 특징과 한계 :
페치 조인을 사용하면 SQL 호출 횟수를 줄여 성능을 최적화 할 수있다. 그리고 페치 조인은 글로벌 로딩 전략보다 우선순위를 가지고 있다. 하지만 전체적으로 봤을때 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악형향을 미칠 수 있다. 따라서 글로벌 로딩 전략은 될 수 있으면 지연로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이라 할 수 있다.
  • 페치 조인 대상에는 별칭을 줄 수 없다.
    그래서 SELECT, WHERE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 페이징 API와 같이 사용할 수 없다.

페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다. 하지만 여러 테이블을 조인해서 필요한 필드들만 조회해서 DTO를 만들어야 되는 경우는 이것이 더 효과적일 수 있다.

 

 


경로 표현식

.(점)을 찍어 객체 그래프를 탐색하는 것이다.

  • 상태 필드(state field) : 단순히 값을 저장하기 위한 필드
  • 연관 필드(association field) : 연관관계를 위한 필드, 임베디드 타입 포함
     - 단일 값 연관 필드 : @XtoOne, 대상이 엔티티
     - 컬렉션 값 연관 필드 : @XtoMany, 대상이 컬렉션
1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Member {
    private String username; // 상태필드
    private String age; // 상태필드
    
    @ManyToOne(...)
    private Team team; // 연관 필드(단일 값 연관필드)
    
    @OneToMany(..)
    private List<Order> orders; // 연관 필드(컬렉션 값 연관필드)
}
cs
  • 상태 필드 : 경로 탐색의 끝. 더는 탐색 불가.
    예) t.username, t.age
  • 단일 값 연관 필드 : 묵시적 내부조인이 일어난다. 계속 탐색 가능
    예) m.team
  • 컬렉션 값 연관 필드 : 묵시적 내부조인이 일어난다. FROM절에서 조인을 통해 별칭을 얻어야만 탐색 가능
    예) m.orders
📌
* 명시적 조인 : JOIN 을 직접 적어주는 것
* 묵시적 조인 : 내부조인이 묵시적으로 일어나는 것

 

상태 필드 경로 탐색

select m.username, m.age from Member m


[SQL]
select m.name, m.age
from Member m

 

단일 값 연관 경로 탐색

select o.member from Order o


[SQL]
select m.*
from Orders o
	inner join Member m on o.member_id = m.id
select o.member.team
from Order o
where o.product.name = 'productA' and o.address.city = 'jinju'


[SQL]
select t.*
from Orders o
inner join Member m on o.member_id = m.id
inner join Team t on m.team_id = t.id
inner join Product p on o.product_id = p.id
where p.name = 'productA' and o.city = 'jinji'

Order엔티티에서 부터 총 3개의 연관관계 탐색에 의한 조인이 일어난다. address 임베디드 타입은 order 테이블에 이미 포함되어 있어서 조인이 발생하지 않는다.

 

컬렉션 값 연관 경로 탐색

📌 컬렉션 값에서 경로 탐색을 시도하는 것이 가장 흔한 실수이다.

→ 경로 탐색을 하고 싶으면 조인을 사용해서 별칭을 얻어야 한다.

select m.username from Team t join t.members m

이렇듯 m 이라는 별칭을 획득하면 다시 탐색을 할 수 있다.

 

 

서브 쿼리

JPQL에서는 서브쿼리를 WHERE, HAVING 절(조건절)에서만 사용할 수 있다.

  • EXISTS : 한개라도 존재하면
  • ALL | ANY | SOME : 모두 만족하거나 || 하나라도 만족하면
  • IN : 하나라도 같은 것이 있으면

 

연산자 우선 순위

  1. 경로 탐색 연산(.)
  2. 수학 연산(+,-,*,/)
  3. 비교 연산(거의 대부분)
  4. 논리 연산(AND, OR, NOT)

 

컬렉션 식

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

  • IS EMPTY : 컬렉션에 값이 비었으면 참
  • MEMBER [OF] : 엔티티나 값이 컬렉션에 포함되어 있으면 참

 

CASE 식

  • 기본 CASE : WHEN 다음 조건절에 조건식이 나오는 경우
  • 심플 CASE : WHEN 다음 조건절에 스칼라식이 나오는 경우
  • COALESCE : 차례대로 조회해서 null이 아니면 반환
  • NULLIF : 두 값이 같으면 NULL을 반환하고 다르면 첫 번째 값을 반환
//기본 CASE
select 
	case when m.age <= 10 then 'a'
    else 'b'
    end
from Member m


//심플 CASE
select
	case t.name when 'a' then 'a'
    			when 'b' then 'b'
                else 'c'
    end
from Team t


//COALESCE (m.username이 null이면 a를 반환
select coalesce(m.username, 'a') from Member m


//nullif (m.username가 b면 null을 반환하고 아니면 m.username반환)
select NULLIF(m.username, 'b') from Member m

 

 

다형성 쿼리

부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.

// 단일 테이블 전략에서의 SQL
select i from Item i


[SQL]
SELECT
	i.*,
    b.*,
    a.*,
    m.*
FROM
	Item i
left outer join
	Book b on i.ITEM_ID = b.ITEM_ID
left outer join
	Album a on i.ITEM_ID = a.ITEM_ID
left outer join
	Movie m on i.ITEM_ID = m.ITEM_ID

 

 

TYPE

조회 대상을 특정 자식의 타입으로 한정하여 사용

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


[SQL]
SELECt i FROM Item i
where i.DTYPE in ('B', 'M')

 

 

엔티티 직접 사용

기본 키 값

엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다.

select count(m.id) from Member m
select count(m) from Member m

같은 의미이다

 

외래 키 값
Team team = em.find(Team.class, 1L);

String qlString = "select m from Member m where m.team = :team");
List resultList = em.createQuery(qlString)
	.setParameter("team", team)
    .getResultList();

해당 쿼리는 팀의 외래 키 값인 1L값이 매핑된다.

String qlSring = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
	.setParameter("teamId", 1L)
    .getResultList();

해당 JPQL과 같은 원리로 동작한다.

 

 

Named 쿼리 : 정적 쿼리

  • 동적 쿼리 : JPQL로 문자를 완성해서 직접 넘기는 쿼리
  • 정적 쿼리 : 미리 정의한 쿼리 Named쿼리라 부르며 한 번 정의하면 변경할 수 없는 정적인 쿼리

✔️ Named 쿼리는 애플리케이션 로딩 시점에 미리 파싱하고 문법을 체크한다. 따라서 오류를 빨리 확인할 수 있고, 성능상 이점도 있다. 데이터베이스의 조회 성능 최적화에도 도움이 된다.

 

Named 어노테이션 정의

1
2
3
4
5
6
7
8
9
10
11
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
public class Member {...}
 
 
List<Member> resultList = em.createNamedQuery("Member.findByUsername",
    Member.class)
        .setParameter("username""Member1")
        .getResultList();
cs

@NamedQuery에 미리 쿼리를 정의하고 createNamedQuery함수를 통해 사용한다.

 

2개 이상의 쿼리 정의

1
2
3
4
5
6
7
8
9
10
@Entity
@NamedQueries({
    @NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"),
    @NamedQuery(
        name = "Member.count",
        query = "select count(m) from Member m")
})
public class Member {...}
cs

 

 

Named 쿼리를 XML에 정의

<named-query name = "Member.findByUsername">
	<query>
    	select m
        from Member m
        where m.username = :username
    </query>
</named-query>

해당 파일을 META-INF/orm.xml 에 생성했다면 별도로 작업을 해주지 않아도 된다.

하지만 새로운 xml 파일(ormMember.xml)에 정의 했다면 persistence.xml에 설정 정보를 추가해주어야 한다.

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

 

어노테이션과 XML에 모두 정의했다면, XML파일이 우선권을 갖게 된다.

 

https://taehoon9393.tistory.com/371

 

JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (2)

QueryDSL : 데이터를 조회하는 데 기능이 특화되어 있다. ✔️ 해당 기능을 사용하기 위해서는 의존성과 라이브러리를 추가 해야한다. pom.xml com.querydsl querydsl-apt 4.4.0 provided com.querydsl querydsl-jpa 4.4.0

taehoon9393.tistory.com