상세 컨텐츠

본문 제목

[SPRING] JPA 프로그래밍 기본 | 객체지향 쿼리 언어

JAVA/기본 & 강의복습

by ranlan 2021. 4. 8. 19:22

본문

728x90

JPA가 지원하는 쿼리 방법

1) JPQL

엔티티 객체를 대상으로 한 쿼리이다.

애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 검색 조건이 포함된 SQL 필요한데 JPA는 SQL을 추상화한 JPQL 이라는 객체 지향 쿼리 언어 제공한다. 테이블이 아닌 엔티티 객체를 대상으로 검색하는 것으로 SQL과 문법이 유사하며 select, from, where, group by, having, join 등을 지원한다.

*SQL을 추상화하기 때문에 특정 데이터베이스 SQL에 의존하지 않는다(데이터베이스 종속적인 함수는 사용 불가).

예시) 18세 이상의 회원 조회

String jpql = "select m from Member m where m.age >= 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

실행되는 Hibernate SQL

select
          m.id as id,
          m.age as age,
          m.name as name,
          m.TEAM_ID as TEAM_ID
from
          Member m
                where
                    m.age>18

 

2) Criteria

문자가 아닌 자바 코드로 JPQL 작성하는 것으로 JPQL 빌더 역할을 하는 JPA 공식 기능이다.

하지만 너무 복잡하고 실용성이 없어 잘 사용되지 않는다.

 

3) QueryDSL

문자가 아닌 자바 코드로 JPQL을 작성하는 JPQL 빌더 역할을 한다.

컴파일 시점에서 문법 오류를 찾을 수 있고 동적 쿼리 작성이 편리하며 단순하고 쉬워서 실무에 권장되는 방법이다.

예시)

JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list = query.selectFrom(m)
			.where(m.age.gt(18))
                        .orderBy(m.name.desc())
                        .fetch();

 

4) 네이티브 SQL

JPA가 제공하는 SQL을 직접 사용하는 것으로 JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능들을 사용 가능하다.

예) 오라클의 connect by, 특정 데이터베이스만 사용하는 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 직접 사용

JPA 사용하면서 JDBC 커넥션 직접 사용하거나 스프링 JdbcTemplate, 마이바티스 등 함께 사용 가능하다. 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요하다.

 

 

 JPQL

객체지향 쿼리 언어로 엔티티 객체를 대상으로 한 쿼리로 특정 데이터베이스에 의존적이지 않고 SQL로 변환되어 실행된다.

 

엔티티와 속성은 대소문자 구분 가능하지만 JPQL 키워드는 대소문자 구분이 불가능하다. 테이블이 아닌 엔티티 이름을 사용해야하며 별칭은 필수이다.

 

반환 타입과 메서드

- TypeQuery 반환 타입이 명확할 때 사용

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

- Query 반환 타입이 명확하지 않을 때 사용

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

- query.getResultList() 결과가 하나 이상일 때 리스트 반환

- query.getSingleResult() 결과가 정확히 하나일 때 단일 객체 반환한다. 결과가 없으면 javax.persistence.NoResultException 발생하고 둘 이상이면 javax.persistence.NonUniqueResultException 발생한다.

 

파라미터 바인딩

- 이름 기준

SELECT m FROM Member m where m.username=:username

query.setParameter("username", usernameParam);

- 위치 기준(지양)

SELECT m FROM Member m where m.username=?1

query.setParameter(1, usernameParam);

 

프로젝션

- select 절에 조회할 대상을 지정하는 것으로 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)은 물론 DISTINT로 중복 제거까지 모두 가능하다.

- Query 타입으로 조회

- Object[] 타입으로 조회

- new 명령어로 조회: 단순 값을 DTO로 바로 조회(*패키지 명을 포함한 전체 클래스 명 입력하고 순서와 타입이 일치하는 생성자 필요)

SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

페이징 API

- setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)

- setMaxResults(int maxResult) : 조회할 데이터 수

String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
				.setFirstResult(10)
                           	.setMaxResults(20)
                            	.getResultList();

 

조인 ON절

- 조인 대상 필터링

# 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'

- 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1 부터)

# JPQL
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name

# SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

 

서브쿼리

[NOT] EXISTS [ANY | ALL | SOME] (subQuery)

서브쿼리에 결과 존재하면 참

- ALL: 모두 만족하면 참

- ANY, SOME: 조건을 하나라도 만족하면 참

[NOT] IN (subQuery)

서브쿼리 결과 중 하나라도 같은 것이 있으면 참

 

* 한계 *

표준 스펙 상 where, having 절에서만 서브쿼리 사용 가능하지만 하이버네이트에서 select 절에서도 지원한다.

하지만 from 절의 서브 쿼리는 현재 JPQL에서 불가능함으로 조인으로 풀 수 있으면 풀어서 해결한다.

 

조건식 CASE

 

COALESCE - 하나씩 조회해서 null이 아니면 반환

select coalesce(m.username,'이름 없는 회원') from Member m

NULLIF - 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

select NULLIF(m.username, '관리자') from Member m

 

사용자 정의 함수

하이버네이트는 사용 전 방언에 추가해야 한다. 사용하는 데이터베이스의 방언을 상속받고 사용자 정의 함수를 등록하여 사용한다.

- 등록

public class MyH2Dialect extends H2Dialect {
  public MyH2Dialect() {
      registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
  }
}
<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>

- 사용

select function('group_concat', i.name) from Item i

 

JPQL 경로 표현식 - .(점)을 찍어 객체 그래프 탐색하는 것

select	m.username # 상태 필드
join	m.team t # 단일 값 연관 필드
join	m.orders o # 컬렉션 값 연관 필드
where	t.name = '팀A'

 

상태 필드 - 단순히 값을 저장하기 위한 필드

연관 필드 1) 단일 값 연관 필드

@OneToOne, @ManyToOne 필드이며 대상이 엔티티로 엔티티와의 연관관계를 위한 필드이다.

묵시적 내부 조인(inner join) 발생한다.

* 묵시적 조인이란 JPQL에 직접적인 join 연산에 대한 언급은 없지만 쿼리에 join 연산이 포함되는 경우를 의미한다. 내부 조인만 가능하다. 반면 명시적 조인은 join 키워드를 직접 사용한 조인 연산을 의미한다.

예1) JPQL - select o.member from Order o

         SQL - select m.* from Orders o inner join Memer m on o.member_id = m.id > 조인 연산 발생

예2) select m.team.name From Member m

         m.team은 연관필드이고 m.team.name은  은 상태필드이다

        > 실제 쿼리 - select t.id, t.name from Member m inner join Team t on m.team_id = t.id

   

연관필드 2) 컬렉션 값 필드

@OneToMany, @ManyToMany 필드이며 대상이 컬렉션으로 컬렉션과의 연관관계를 위한 필드를 의미한다.

묵시적 내부 조인이 발생하고 해당 엔티티의 추가적인 탐색은 불가능하다. → 별칭을 달아 추가 탐색 가능하다.

예) select m from team t join t.members m

 

* 항상 내부 조인 해야함

컬렉션 값 필드는 경로 탐색의 끝에 명시적 조인을 통해 별칭을 얻어야 한다. 경로 탐색은 주로 select, where 절에서만 사용하나 묵시적 조인으로 인해 sql의 from 절에 영향을 준다. 따라서 묵시적 내부 조인은 쿼리 튜닝시 어려우며 성능에도 영향을 미침으로 권장하지 않는다.

 

 

 

728x90

관련글 더보기

댓글 영역