WEB/JPA

김영한 (ORM 표준 JPA 프로그래밍 10) 객체지향 쿼리 언어 소개

Tony Lim 2021. 3. 18. 14:31
728x90

 

JPQL

검색을 할 떄도 테이블이 아닌 엔티티 객체를 대상으로 검색 -> 데이터베이스 SQL에 의존 하지 않는다.

애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요

SQL과 문법유사, select ,from , where , group by, having , join지원

            List<Member> resultList = em.createQuery(
                    "select m From Member m where m.username like '%kim%'",
                    Member.class
            ).getResultList();

객체 Member 로부터 조회하는 것을 확인 할 수 있다. 또한 JPQL 은 단순한 String 임으로 validate하기 어렵다.

Hibernate: 
    /* select
        m 
    From
        Member m 
    where
        m.username like '%kim%' */ select
            member0_.MEMBER_ID as member_i1_7_,
            member0_.createdBy as createdb2_7_,
            member0_.createdDate as createdd3_7_,
            member0_.lastModifiedBy as lastmodi4_7_,
            member0_.lastModifiedDate as lastmodi5_7_,
            member0_.city as city6_7_,
            member0_.street as street7_7_,
            member0_.zipcode as zipcode8_7_,
            member0_.LOCKER_ID as locker_12_7_,
            member0_.TEAM_ID as team_id13_7_,
            member0_.USERNAME as username9_7_,
            member0_.endDate as enddate10_7_,
            member0_.startDate as startda11_7_ 
        from
            Member member0_ 
        where
            member0_.USERNAME like '%kim%'

이런식으로 내부적으로 native sql 을 보내준다.

 

QueryDSL

문자가 아닌 자바코드로 JPQL을 작성할 수 있음

컴파일 시점에 문법 오류를 찾을 수 있다.

동적 쿼리 작성이 편리하다.

 

# em.flush() 는 commit()직전에도 날라가지만 query (예를들면 createNativeQuery ) 가 날라갈 때도 호출이된다. 하지만 JPA기술 관련이 아닌것을 사용하면 flush()가 호출이 안됨으로 manual 하게 호출해줘야한다.

 

기본 문법과 쿼리 API

JPQL 문법

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

엔티티와 속성은 대소문자 구분 한다.(Member, age)

JPQL 키워드는 대소문자 구분 하지 않는다.(select , from , where) 

엔티티 이름 사용, 테이블 이름이 아님 (Member) 

별칭은 필수(m) as는 생략이 가능하다.

 

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

Query =변환 타입이 명확하지 않을 때 사용

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

말 그대로다 Query의 경우에는 username, age를 동시에 반환하기 타입이 명확하지 않다.

 

결과 조회 API

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

  • 결과가 없으면 빈 리스트 반환

query.getSingleResult() = 결과가 정확히 하나, 단일 객체 반환

  • 결과가 없으면 javax.persistence.NoResultException
  • 둘 이상이면 javax.persistence.NonUniqueResultException

Spring Data JPA에서는 Null, Optional 로 반환해준다. Exception을 터지지 않게 해줌
내부적으로 try catch 로 잡아서 return 해주는것이다.

 

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

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

위치기반은 쓰지 않는다. 위에처럼 이름 기준을 쓰도록 하자.

 

프로젝션

select 절에 조회할 대상을 지정하는것

엔티티, 임베디드 타입, 스칼라 타입 (숫자, 문자등 기본데이터 타입)

Distinct 로 중복 제거가능

select m from Member m

엔티티 프로젝션으로 여기서 나오는 모든 List<Member> 들은 영속성관리가 된다. 즉 mebmer.setage(10) 이런식으로 바꿔주면 알아서 더티체크 해서 update 쿼리를 날려준다. 

그래서 이름이 엔티티 프로젝션이다. 불러온 entity들은 다 persistence context의 관리대상이다.

 

프로젝션 - 여러 값 조회

select m.username, m.age from Member as m

new 명령어로 조회. 

단순값을 DTO로 바로 조회

            List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username,m.age)  from Member m ", MemberDTO.class)
                    .getResultList();

패키지명을 포함한 전체 클래스 명 입력해야한다.

순서와 타입이 일치하는 생성자가 필요하다.

 

페이징 API

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

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

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

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

내부적으로 database 방언에 맞게 페이징 쿼리를 날려준다.

 

조인

내부조인 

select m from Member m [Inner] JOIN m.team t

회원은 있고 team이 없는 경우에는 -> 조회가 되지 않음

 

외부조인 

select m from Member m LEFT [Outer] JOIN m.team t

회원은 있고 team이 없는 경우에는 -> team 이 null인상태로 column이 생김

 

세타조인  

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

아무 연관관계 없는 테이블을 조회 할때 주로 쓰임 

 

조인 대상 필터링

예) 회원과 팀을 조인하면서, 팀 이름이 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"

 

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

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

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

 

서브 쿼리

나이가 평균보다 많은 회원

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는 Where , Having 절에서만 서브 쿼리 사용 가능

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

From 절의 서브 쿼리는 현재 JPQL에서 불가능하다. 

 

조건 - 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 반환, 다르면 첫번쨰 값 반환

사용자 이름이 '관라자' 면 null을 반환하고 나머지는 본인의 이름을 반환

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

 

사용자 정의 함수 호출

하이버네이트는 사용전 방언에 추가 해야한다. 

사용하는 DB방언을 상속 받고, 사용자 정의 함수를 등록한다. 

public class MyH2Dialect extends H2Dialect
{
    public MyH2Dialect()
    {
        registerFunction("group_concat",new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

 

이것은 실제 H2Dialect 소스코드들을 참조해서 작성하면된다. 불러올수 있는 함수들은 현재 H2 를 쓰고있으니 안에다 function 을 정의해서 쓰면된다. 그후에는 persistance.xml 에서             <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
이것을 MyH2Dialect로 바꾸어주면된다.

            String query = "select function('group_concat',m.username) from Member m";
            List<String> query1 = em.createQuery(query, String.class)
                    .getResultList();
            for (String s : query1)
            {
                System.out.println("s = " + s);
            }
Hibernate: 
    /* select
        function('group_concat',
        m.username) 
    from
        Member m */ select
            group_concat(member0_.username) as col_0_0_ 
        from
            Member member0_
s = member1,member2

결과물로 member의 username 이 이어져서 나왔다.

 

 

 

 

728x90