JPA - 3

Single Ko·2023년 6월 9일
0

jpa

목록 보기
3/8

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

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL 필요.
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
  • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리
  • JPQL은 SQL로 변환된다.
String jpql = "select m From Member m where m.name like ‘%hello%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
String jpql = "select m from Member m where m.age > 18";
 List<Member> result = em.createQuery(jpql, Member.class)
 .getResultList();
 
 실행된 SQL
 
 select
 	m.id as id,
 	m.age as age,
	 m.USERNAME as USERNAME,
	 m.TEAM_ID as TEAM_ID
 from
 	Member m
 where
	 m.age>18 
  • 문제는 JPQL을 짜기 위해서는 string을 쳐야된다는 것.
  • 오타를 잡기 힘들고, 잘못 쳐도 컴파일러가 체크도 못해준다. 또 줄을 넘겼을때 띄어쓰기도 신경 써줘야 되는 문제
  • 이런 점이 합쳐저 동적 쿼리를 짜기 매우 힘들다.

Criteria

장점
문자가 아닌 자바코드로 JPQL을 작성할 수 있음
JPQL 빌더 역할
JPA 공식 기능
동적 쿼리 짜기 쉬움

단점
너무 복잡하고 실용성이 없다.
Criteria 대신에 QueryDSL 라이브러리 사용 권장

QueryDSL

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();
 
  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
  • 실무 사용 권장

네이티브 SQL

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

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

JDBC 직접 사용, SpringJdbcTemplate 등

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

JPQL

select m from Member as m where m.age > 18

엔티티와 속성은 대소문자 구분O (Member, age)
JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
엔티티 이름 사용, 테이블 이름이 아님(Member)
별칭은 필수(m) (as는 생략가능)
클래스 이름이 같으면 클래스 이름을 바꿔라..

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

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
  • Spring Data JPA를 쓴다면 return null을 반환하는 식으로 한다. exception이 터지는게 그렇게 좋지 않음..

프로젝션

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로 중복 제거 가능
  1. Query 타입으로 조회
  2. Object[] 타입으로 조회
  3. new 명령어로 조회
1. List resultList = em.createQuery("select m.username, m.age from Member m)
	.getResultList();
 Object o = resultList.get(0);
 Object[] result = (Object[]) o;
 
2. List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m)
.getResultList();
Object[] result = resultList.get(0);

3. List<MemberDTO> em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", 
		MemberDTO.class).getResultList();
        
 MemberDTO memberDTO = result.get(0);
  • new 명령어 조회를 사용하는 편이다. 다만 한가지 아쉬운점은 패키지 명을 포함한 전체 클래스 명 입력이 필요하고, 순서와 타입이 일치하는 생성자 필요하다.

페이징 API

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

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터시작..)
  • setMaxResults(int maxResult) : 조회할 데이터 수
String jpql = "select m from Member m order by m.age desc";

 List<Member> resultList = em.createQuery(jpql, Member.class)
 		.setFirstResult(1)
 		.setMaxResults(10)
 		.getResultList();

조인

  • 내부 조인 inner join

  • 외부 조인 left join

  • 세타 조인 cross join

조인 - ON 절

  • ON절을 활용한 조인(JPA 2.1부터 지원)
    1. 조인 대상 필터링
    1. 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

서브 쿼리

  • 나이가 평균보다 많은 회원
    select m from Member m
    where m.age > (select avg(m2.age) from Member m2)

  • 한 건이라도 주문한 고객
    select m from Member m
    where (select count(o) from Order o where m = o.member) > 0

지원 함수

  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
  • {ALL | ANY | SOME} (subquery)
  • ALL 모두 만족하면 참
  • ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

팀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에서 불가능 (1.조인으로 풀 수 있으면 풀어서 해결 , 2. 쿼리문 2번 날려서 해결, 3.네이티브 쿼리 날려서 해결)

JPQL 타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
String query = "select m.username, 'HELLO', true, FROM Member m " +
			   "WHERE m.type = :userType";
               
List<Object[]> result = em.createQuery(query).setParameter("userType", MemberType.ADMIN)
 	.getResultList();
   
em.createQuery("select i from Item i where type(i) = Book",Item.class)
	.getResultList();
  • enum type을 사용하려면 패키지에 클래스명까지 다 적어 줘야된다.
  • 하지만, :userType 같이 parameter로 던지면 밑에서 쉽게 넣을 수 있긴 함.

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이 아니면 반환
• NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m

사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m

JPQL의 함수

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

사용자 정의 함수

public class MyH2Dialect extends H2Dialect {
	public MyH2Dialect() {
    	registerFunction("group_concat",
        		new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}
  • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
SELECT FUNCTIOIN('group_concat', m.username) FROM Member m
SELECT group_concat(m.username) FROM Member m;

경로 표현식

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

select m.username -> 상태 필드
 from Member m
 join m.team t -> 단일 값 연관 필드
 join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A
  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)
    • 경로 탐색의 끝, 탐색x
  • 연관 필드(association field): 연관관계를 위한 필드
    • 단일 값 연관 필드:
      @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
      묵시적 내부 조인(inner join) 발생, 탐색O
    • 컬렉션 값 연관 필드:
      @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)
      묵시적 내부 조인 발생, 탐색X
      FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
      select t.members.username from Team t => 실패. 탐색 불가능.
      select m.username from Team t join t.members m => 탐색 가능.

사실 위의 것은 다 무시하면 된다. 실제로는 묵시적 조인이 뜨게 사용하면 안되고, 명시적 조인을 사용하도록 해라.(이렇게 해야, 더 명확하게 나온다.)

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

항상 내부 조인

컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함

경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

가급적 묵시적 조인 대신에 명시적 조인 사용

조인은 SQL 튜닝에 중요 포인트

묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

profile
공부 정리 블로그

0개의 댓글