JPA

JPQL(Java Persistence Query Language)

salmon16 2024. 1. 3. 20:46

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

 

1. jpql

2. jpa criteria

3. queryDSL

4. 네이티브 SQL

5. JDBC API 직접 사용, MyBatis, SpringJdbcTemplate함께 사용

 

jpql = 동적 쿼리 힘들다.

실무에서 문자열 +힘들다 

criteria = sql 스럽지 않다, 유지보수 어렵다.  

 

JPQL

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리를 작성하는 것이 아니라 엔티티 객체를 대상으로 쿼리를 작성한다.
  • JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.(매핑 정보와 방언을 조합해서)

JPQL 문법

  • 엔티티와 속성은 대소문자 구분한다.(Member)
  • JPQL 키워드는 대소문자 구분을 하지 않는다 (select, from, where)
  • 엔티티 이름을 사용한다. 테이블이름이 아니다
  • 별칭은 필수적으로 사용해야 한다. (Member m) as 생략가능
  • 집합과 정렬을 사용가능하다 (COUNT, SUM,  AVG, MAX, MIN, GROUP BY, HAVING, ORDER BY)

TypeQuery vs Query

TypeQuery : 반환 타입이 명확할 때 사용한다.

Query : 반환 타입이 명확하지 않을 때 사용한다.

 

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
Query query2 = em.createQuery("select m.username, m.age from Member m");

query1의 경우 Member.class로 타입이 명확하다 하지만 query2의 경우 username은 String, age는 int이므로 타입이 명확하지 않아 TypedQuery를 사용하지 못한다. 만약 username만 select 하는 경우에는 String으로 TypedQuery를 사용할 수 있다.

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환 
    • 결과가 없으면 빈 리스트를 반환한다.
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query1.getResultList();
for (Member member1 : resultList) {
    System.out.println("member1 = " + member1);
}
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면 : javax.persistence.NoResultException
    • 둘 이상이면 : javax.persistence.NonUniqueResultException
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
Member singleResult = query1.getSingleResult();
System.out.println("singleResult = " + singleResult);

 

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

이름 기준 파라미터 바인딩을 사용하려면 쿼리에 : 표시를 사용한 후 변수 명을 작성해 주고 setParameter 메서드를 사용하면 된다.

Member singleResult = em.createQuery("select m from Member m where m.username = :username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult();

 

위치 기반은 ? 표시를 사용한 후 위치를 나타내는 숫자를 적어서 사용 후 serParameter 메서드를 사용해서 값을 할당해 주면 된다.

Member singleResult = em.createQuery("select m from Member m where m.username = ?1", Member.class)
        .setParameter(1, "member1")
        .getSingleResult();

※ 위치 기준은 대도록 사용하지 않는 것을 권장한다 왜냐하면 쿼리를 수정 시 바인딩을 추가했을 때 뒤에 위치의 순서들이 다 바뀌기 때문에 모두 수정해주어야 하기 때문이다.

프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상:  엔티티, 임베디드 타입, 스칼라 타입
  • SELECT m.team FROM Member m -> 엔티티 프로젝트
  • DISTINCT로 중복 제거가능 

위 사진과 같이 em.createQuery로 얻은 result의 Member들은 다 영속성 컨텍스트에서 다 관리가 된다.

즉 setUsername을 했을 때 update 쿼리가 나가게 된다.

반환 타입이 명확하지 않을 때

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

1. 위 쿼리 처럼 반환타입이 명확하지 않을 땐 Object[]을 return 한다.

List resultList = em.createQuery("select m.username, m.age from Member m ").getResultList();

Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

2. MemberDto클래스를 생성해서 MemberDto 클래스의 타입으로 받는다

public class MemberDto {
    private String username;
    private int age;
List<MemberDto> resultList = em.createQuery("select new org.example.MemberDto(m.username, m.age) from Member m", MemberDto.class)
        .getResultList();

MemberDto memberDto = resultList.get(0);
System.out.println("username = " + memberDto.getUsername());
System.out.println("age = " + memberDto.getAge());

MemberDto는 Entity가 아니므로 sql에서 사용하려면 new로 새로 할당해주어야 한다. 이때 패키지명을 다 적어야 한다. (org.example)

이때 MemberDto의 생성자에는 sql에서 사용하는 생성자와 순서, 타입이 일치하는 생성자가 필요하다.

페이징

  • JPA는 페이징을 다음 두 API로 추상화한다.
  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
for (int i = 0;i < 100;i++) {
    Member member = new Member();
    member.setUsername("member1");
    member.setAge(i);
    em.persist(member);
}

em.flush();
em.clear();

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

System.out.println("result.size() = " + resultList.size());
for(Member member1: resultList) {
    System.out.println("member = " + member1);
}

조인

  • 내부 조인: SELECT m FROM Member m JOIN m.team t
  • 외부 조인: SELECT m FROM Member m LEFT JOIN m.team t
  • 세타 조인: SELECT count(m) from Member m, Team t where m.username = t.name

조인 - ON 절

  • ON절을 활용한 조인(JPA 2.1부터 지원)
    • 조인 대상 필터링
    • 연관관계없는 엔티티 외부 조인 (하이버네이트 5.1부터) 
  • ex) 
    • SELECET m, t FROM Member m LEFT JOIN m.team t ON t.name = 'A'
    • SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name(연관관계 x)

서브 쿼리

서브 쿼리는 쿼리 안에 쿼리가 들어가는 쿼리이다

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

selext m from Member m

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

 

서브 쿼리 지원 함수

  • [NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
    • ALL/ANY/SOME
  • [NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참 
  • ex) select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리를 사용할 수 있다.
  • 하지만 하이버네이트에서는 SELECT 절도 지원한다.
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다.
    • 조인으로 풀 수 있으면 풀어서 해결
    • 안되면 sql 두 번 날린다 or native sql을 사용 or service로직에서 처리한다.
    • hibernate 6부터는 from절의 서브 쿼리도 지원한다.

JPQL 타입 표현

  • 문자 : 'Hello'
  • 숫자 : 10L, 10D, 10F
  • Boolean : TRUE, FALSE
  • ENUM : jpabook.Member.Type.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

조건식 - 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%'
    end
from Team t
  • COALESCE: 하나씩 조회해서 null이 아니면 반환한다. 
Member member1 = new Member();
            member1.setUsername("member1");
            member1.setAge(10);

            Member member2 = new Member();
            member2.setUsername(null);
            member2.setAge(10);
            em.persist(member1);
            em.persist(member2);
            em.flush();
            em.clear();

            List<String> resultList = em.createQuery("select coalesce(m.username, '이름 없는 회원') from Member m ", String.class).getResultList();
            for(String name: resultList) {
                System.out.println("member = " + name);
            }

 

username이 null이 아니면 member1이 null이면 이름 없는 회원으로 조회된다.

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

JPQL 기본 함수

JPQL이 제공하는 기본 함수는 데이터베이스에 상관없이 쓸 수 있다.

  • CONCAT : 문자열 두 개를 더하는 함수이다. 
    • "select concat('a', 'b') from Member m" -> 출력 ab
  • SUBSTRING : 문자열을 자른다.
    • "select substring(m.username, 2, 3) From Member m"
  • TRIM : 공백 제거
  • LOWER, UPPER : 대소문자 변환
  • LENGTH : 문자열 길이
  • LOCATE : 문자열의 위치를 찾는다.
    • "select locate('de', 'abcdegf') From Member m" -> 출력 4
  • ABS, SQRT, MOD : 수학 함수
  • SIZE, INDEX 
    • "select size(t.members) From Team t" -> 컬렉션 크기를 출력해 준다.

 

 

'JPA' 카테고리의 다른 글

JPA MultipleBagFetchException에러 해결하기  (0) 2024.01.20
값 타입, 값 타입 컬렉션  (0) 2023.09.01
임베디드 타입  (0) 2023.08.31
기본값 타입  (0) 2023.08.31
영속성 전이 : CASCADE, 고아객체  (0) 2023.08.31