[JPA 기본] 객체지향 쿼리 언어 ① - 기본 문법 👨‍💻

홍정완·2022년 11월 12일
0

JPA

목록 보기
37/38
post-thumbnail

소개


✅ JPA는 다양한 쿼리 방법을 지원

  • JPQL
  • JPA Criteria
  • QueryDSL
  • Native SQL
  • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

✅ JPQL 소개

  • 가장 단순한 조회 방법
    • EntityManager.find()
    • 객체 그래프 탐색(a.getB().getC())
  • ❓ 나이가 18살 이상인 회원을 모두 검색하고 싶다면

✅ JPQL - 필요성

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요

✅ JPQL - 특징

  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리를 제공
String sql = "select m from Member.class as m where m.username like '%k%'";

  • 일반 SQL과 비슷하지만 엔티티를 대상으로 한다는 점이 다르다.
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • 동적 쿼리 생성이 쉽지 않다.



queryDSL - 소개, 오픈소스 라이브러리

// JPQL
// select m from Member m where m.age > 18
JPAFactoryQuery queryFactory= new JPAQueryFactory(em); 
QMember m = QMember.member;
List<Member> list = queryFactory.selectFrom(m) 
						.where(m.age.gt(18))
						.orderBy(m.name.desc())
						.fetch()
  • 문자가 아닌 자바 코드로 JPQL을 작성할 수 있다.
  • JPQL 빌더 역할
  • 컴파일 시점에서 문법 오류를 찾을 수 있음

...
List<Member> list = query.selectFrom(m) 
						 .wheree(m.age.gt(18)) // 컴파일 시점에서 오류 검출 가능
						 .orderBy(m.name.desc())
						 .fetch()
  • 동적 쿼리 작성 편리
  • 단순하고 쉽다.
  • 실무 사용 권장



네이티브 SQL 소개

  • JPA가 제공하는 SQL을 직접 사용하는 기능
  • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
    • ex) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql ="SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList = em.createNativeQuery(sql, Member.class)
							.getResultList();



JDBC 직접 사용, SpringJdbcTemplate 등

  • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등 함께 사용 가능
  • 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
    • ex) JPA를 우회해서 SQL을 실행하기 직전 영속성 컨텍스트를 수동 플러시 해줘야 한다.
Member member = new Member();
member.setUsername("catsbi");

conn.createQuery("select * from Member where username = 'catsbi'");
// 결과 없음

member는 Jdbc가 쿼리를 수행하는 시점에서 영속성 컨텍스트에만 있고 db에 아직 저장되지 않았기 때문에 조회 결과가 없다 그러므로 쿼리 수행 전 수동으로 플러시를 해줘야 한다.



기본 문법과 쿼리 API


JPQL 소개

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리 한다.
  • JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
    • 조회 기능을 만들 때 특정 DB에 의존하는 SQL을 따로 안 만들어도 된다.
  • JPQL은 결국 SQL로 변환된다.



객체/DB 모델

  • UML

JPQL 문법 - 기본 SQL 문법과 동일

  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분을 한다.
  • JPQL 키워드는 대소문자 구분을 하지 않는다. (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님 (Member)
  • 별칭은 필수(alias) (as는 생략 가능)

💡 집합과 정렬

  • count(m), sum(m.age), avg(m.age), max(m.age), min(m.age)
  • group by, having



TypeQuery, Query


  • TypeQuery : 반환 타입이 명확할 때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Meber m", Member.class);
  • Query : 반환 타입이 명확하지 않을 때 사용
Query query = em.createQuery("SELECT m.username, m.age FROM Meber m");



결과 조회 API

  • query.getResultList() : 결과가 하나 이상일 때 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult() : 결과가 정확히 하나

    • 결과가 하나만 나오는 것 외의 모든 상황에서 에러가 나오기 때문에 사용에 주의가 필요.
    • 결과가 없으면 : javax.persistence.NoResultException
    • 둘 이상이면 : javax.persistence.NonUniqueResultException

파라미터 바인딩 - 이름 기준



프로젝션(SELECT)


  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입 (숫자, 문자 등 기본 데이터 타입)
  • SELECT m FROM Member m → 엔티티 프로젝션
  • SELECT m.team FROM Member m → 엔티티 프로젝션
  • SELECT m.address FROM Member m → 임베디드 타입 프로젝션
  • SELECT m.username, m.age FROM Member m → 스칼라 타입 프로젝션
  • DISTINCT로 중복 제거



💡 묵시적 조인, 명시적 조인


묵시적 조인

List<Team> result = em.createQuery("select m.team from Member m", Team.calss)
// SQL : SELECT t.id, t.name FROM Member m inner join TEAM t on m.team_id = t.id
  • 위 코드처럼 JPQL에는 JOIN 문법이 없지만 자연스럽게 JOIN을 해서 Team Entity를 조회해 온다.

명시적 조인

List<Team> result = em.createQuery("select t from Member m join m.team t", Team.calss)
// SQL : SELECT t.id, t.name FROM Member m inner join TEAM t on m.team_id = t.id
  • 실행되는 SQL은 동일하지만 명시적으로 JPQL에 적어줬기에 가독성이 높고 JOIN 쿼리 예측이 가능하다.
  • 묵시적 조인보다는 명시적 조인을 하는 게 좋다.



임베디드 타입 프로젝션

em.createQuery("select o.address from Order o", Address.calss).getRresultList();
// SQL : SELECT o.city, o.street, o.zipcode FROM ORDERS o

임베디드 타입은 따로 조인을 해서 가져오지는 않는다. 다만, from 절에 Order가 아닌 Address를 적으면 에러가 난다. 엔티티로부터 시작돼야 한다.



스칼라 타입 프로젝션 / 여러 값 조회

  • 데이터의 타입과 상관없이 여러 개를 가져오는 방법
em.createQuery("select distinct m.address, m.age from Member m").getRresultList();
  1. Query 타입으로 조회

  2. Object[] 타입으로 조회

  1. new 명령어로 조회 - 단순 값을 DTO로 바로 조회
    SELECT new jpabook.jpql.UserDTO(m.username, m.age)from Memer m;
    패키지 명을 포함한 전체 클래스 명을 적어줘야 한다.
    순서와 타입이 일치하는 생성자 필요.



페이징


  • JPA는 페이징을 다음 두 API로 추상화
  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
em.createQuery("select m from Member m order by m.age desc", Member.class)
					.setFirstResult(0)
					.setMaxResults(10)
					.getReulstList();

기존에는 Diarect 별로 방언을 맞춰서 쿼리를 직접 구현해야 했는데, 이제는 두 개의 함수로 해결할 수 있다.



조인


내부 조인

SELECT m FROM Member m [INNER] JOIN m.team t

외부 조인

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

세타 조인

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

조인 - ON 절


조인 대상 필터링

ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

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

연관관계없는 엔티티 외부 조인

ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

  • 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.username



서브 쿼리


ex) 나이가 평균보다 많은 회원

select m from Member m
where m.age > (select avg(m2.age) from Member m2);

ex) 한 건이라도 주문한 고객

select m from Member m
where (select count(o) from Order o where m=o.member)>0

서브 쿼리 지원 함수


예제

  • 팀 A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = '팀A')
  • 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o
where o.orderAmount > ALL(select p.stockAmount from Product p)
  • 어떤 팀이든 팀에 소속된 회원
select m from Member m
where m.team = ANY(select t from Team t)

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능

  • SELECT 절도 가능 (하이버네이트에서 지원)

  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결



JPQL 타입 표현과 기타식


  • 문자 : 'HELLO', 'SHE"S'
  • 숫자 : 10L (Long), 10D (Double), 10F (Float)
  • Boolean : TRUE, FALSE

  • ENUM : jpabook.MemberType.Admin (패키지명 포함)
select m.username, 'HELLO', true from Member m 
where m.type = jpql.MemberType.ADMIN

  • 엔티티 타입 : TYPE(m)= Member (상속 관계에서 사용)
em.createQuery("select i from Item i where type(i) = Book", Item.class);



JPQL 기타

  • SQL과 문법이 같은 식
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, ≥, <, ≤, <>
  • BETWEEN, LIKE, IS NULL



조건식 - CASE 식


기본 CASE 식

select case when m.age <= 10 then '학생 요금'
			when m.age >= 60 then '경로 요금'
			else '일반 요금'
	   end
from Member m



단순 CASE 식

select case t.name
		  when '팀 A' then '인센티브 110%'
		  when '팀 B' then '인센티브 120%'
		  else '인센티브 105%'
	   end
from Team t

  • COALESCE : 하나씩 조회해서 null이 아니면 반환
select coalesce(m.username, '이름 없는 회원') from Member m;
  • NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
select NULLIF(m.username, '관리자') from Member ;



JPQL 함수


  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE INDEX (JPA용도)

// CONCAT
select concat('a','b'); // ab

// SUBSTRING : firstParam의 값을 secondParam 위치부터 thirdParam 개수만큼 잘라서 반환
select substring('abcd', 2,3) // bc

// TRIM
select trim(' lee han sol ') // lee han sol

// LOWER, UPPER
select LOWER('hansolHI'); // hansolhi
select UPPER('hansolHI'); // HANSOLHI

// LENGTH
select LENGTH('hansolHI'); // 6

// LOCATE
select LOCATE('so', 'hansol'); // 4

// ABS, SQRT, MOD
select ABS(-30); // 30
select SQRT(4); // 2
select MOD(4,2); // 0

// SIZE, INDEX (JPA 용도)
select SIZE(t.members) from Team t // 0

사용자 정의 함수 호출


  • 하이버네이트는 사용 전 방언에 추가해야 한다.
    • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
      실제 소스코드 내부에 정의돼있는 함수들을 참고해서 작성해 주면 된다.
// group_concat이라는 함수를 만들어서 등록한다고 가정한다.
public class MyPostgresDialect extends PostgreSQL94Dialect {
    public MyPostgresDialect() {
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
...
...
...
// 설정 파일 등록
<property name="hibernate.dialect" value="jpql.MyPostgresDialect"/>
profile
습관이 전부다.

0개의 댓글