본문 바로가기

Dot Programming/JPA

[JPA] 객체지향 쿼리 언어1 - 기본 문법

    1. 객체지향 쿼리 언어 소개

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

       ∙ JPQL

       ∙ JPA Criteria

       ∙ QueryDSL 

       ∙ 네이티브 SQL

       • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

     

    ☛ JPQL이라는 표준 문법을 기반으로 한 JPA Criteria, QueryDSL는 자바코드를 설계하여 JPQL을 빌드하는 제네레이터 클래스 모음이다.

    ☛ 대부분 JPQL로 해결가능하지만 어쩌다 안될 경우에는 네이티브 SQL, MyBatis, SpringJdbcTemplate사용하면 된다.

     

    JPA에서 가장 단순한 조회 방법

     1) EntityManager.find()

     2) 객체 그래프 탐색(a.getB().getC())

     

    만약 나이가 18살 이상인 회원을 모두 검색하고 싶다면?

    이럴 때 JPQL 쿼리를 사용하여 조회를 해야한다.

     

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

     

    JPQL

    JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.

    SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원한다.

     

    JPQL vs SQL

      → JPQL은 엔티티 객체를 대상으로 쿼리

      → SQL은 데이터베이스 테이블을 대상으로 쿼리

     

    테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다. SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

    List<Member> result = em.createQuery(
            "select m From Member m where m.name like '%kim%'", Member.class)
            .getResultList();
    
    for(Member member : result){
        System.out.println("member = " + member);
    }

    ☛ JPQL을 한마디로 정의하면 객체 지향 SQL이다.

     

    Criteria

    JPA 공식 기능으로 JPQL 빌더 역할을 한다.

    Criteria는 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.

    //Criteria 사용 준비
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Member> query = cb.createQuery(Member.class);
    
    //루트 클래스 (조회를 시작할 클래스)
    Root<Member> m = query.from(Member.class);
    
    //쿼리 생성
    CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
    List<Member> resultList = em.createQuery(cq).getResultList();
    

    🙅‍♂️ 단점: 너무 복잡하고 실용성이 없다.

        ☛ Criteria 대신에 QueryDSL 사용 권장

     

    QueryDSL

    Criteria와 마찬가지로  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();

    🙆‍♂️ 장점 : 단순하고 쉬움

        ☛  QueryDSL 실무 사용 권장

     

    네이티브 SQL

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

    예) 오라클 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, 마이바티스등을 함께 사용 가능하다

    ☛ 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요하다.

    예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시

     

    2. JPQL -  기본 문법과 기능

    JPQL 

    JPQL은 객체지향 쿼리 언어이다.

      →  따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

     

    JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다. JPQL은 결국 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

     → GROUP BY, HAVING, ORDER BY도 SQL과 똑같이 쓰면 됨

     

    TypeQuery, Query

    • 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");

     

    결과 조회 API

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

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

     

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

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

     

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

    ∙ 이름 기준으로 사용 

    SELECT m FROM Member m where m.username=:username
    query.setParameter("username", usernameParam);

     

    ∙ 위치 기준은 웬만하면 사용 x -> 중간에 순서 끼워넣으면 장애 발생

    SELECT m FROM Member m where m.username=?1 
    query.setParameter(1, usernameParam);

     

    3. 프로젝션 (SELECT)

    프로젝션은 SELECT절에 조회할 대상을 지정하는 것이다.

    • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)

     

    1) SELECT m FROM Member m -> 엔티티 프로젝션

    List<Member> result = em.createQuery("select m From Member m", Member.class)
                .getResultList();
    Member findMember = result.get(0);
    findMember.setName("change"); // 변경됨 (영속성 컨텍스트 관리 됨)

      → getResultList()로 나온 데이터들도 영속성 컨텍스트에 관리된다.

     

    2) SELECT m.team FROM Member m -> 엔티티 프로젝션

    List<Team> result = em.createQuery("select m.team From Member m", Team.class)
                        .getResultList();

      → SQL 잘 다루면 조인이 예측될 수 있지만 운영하면서 헷갈릴 여지가 있기 때문에 조인은 웬만하면 명시적으로 표현하는 것이 좋다.

    List<Team> result = em.createQuery("select t From Member m join m.team t", Team.class)
                        .getResultList();

     

    3) SELECT m.address FROM Member m -> 임베디드 타입 프로젝션

    em.createQuery("select m.address From Member m", Member.class)
                        .getResultList();

      → address(임베디드 값 타입)을 혼자 호출할 수 없기 때문에 소속되어있는 엔티티를 통해 호출

     

    4) SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션 DISTINCT로 중복 제거

    // SELECT m.username, m.age FROM Member m
    em.createQuery("select distinct m.username, m.age From Member m", Member.class)
    		.getResultList();

     

    프로젝션 - 여러 값 조회

    SELECT m.username, m.age FROM Member m

        1. Query 타입으로 조회

        2. Object[] 타입으로 조회

        3. new 명령어로 조회

           →  단순 값을 DTO로 바로 조회

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

           →  패키지 명을 포함한 전체 클래스 명 입력

           →  순서와 타입이 일치하는 생성자 필요

     

    4. 페이징 API

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

      • setFirstResult(int startPosition) : 조회 시작 위치
 (0부터 시작)
      • setMaxResults(int maxResult) : 조회할 데이터 수

    //페이징 쿼리
    for(int i=0; i<100; i++) {
        Member member = new Member();
        member.setName("member"+i);
        member.setAge(i);
        em.persist(member);
    }
    
    em.flush();
    em.clear();
    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();
    System.out.println("resultList.size() = " + resultList.size());
    for(Member member : resultList){
        System.out.println("member = " + member);
    }

     

    페이징 API - MYSQL

    <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServer2012Dialect"/>
    SELECT
    	M.ID AS ID,
    	M.AGE AS AGE,
    	M.TEAM_ID AS TEAM_ID,
        M.NAME AS NAME
    FROM
    	MEMBER M
    ORDER BY
    	M.NAME DESC LIMIT ?, ?

     

    페이징 API - Oracle

    <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect"/>
    SELECT * FROM
    	( SELECT ROW_.*, ROWNUM ROWNUM_
    	FROM
    		( SELECT
    			M.ID AS ID,
    			M.AGE AS AGE,
    			M.TEAM_ID AS TEAM_ID,
                M.NAME AS NAME
    		FROM MEMBER M
            ORDER BY M.NAME
    		) ROW_
    	WHERE ROWNUM <= ?
    	)
    WHERE ROWNUM_ > ?

     

    5. 조인

    내부 조인

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

    String query = "select m from Member m inner join m.team t";
    List<Member> resultList = em.createQuery(query, Member.class)
    						.getResultList();

    → inner생략가능

    • 외부 조인

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

     String query = "select m from Member m left join m.team t";
     List<Member> resultList = em.createQuery(query, Member.class)
                        .getResultList();

    세타 조인

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

    String query = "select m from Member m, Team t where m.useranme = t.name";
    List<Member> resultList = em.createQuery(query, Member.class)
                        .getResultList();

    → 연관관계가 없는 객체를 같이 꺼내어 비교할 때 사용

     

     

    조인 - ON 절

    ON절을 활용한 조인(JPA 2.1부터 지원)

     

    1. 조인 대상 필터링

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

       

    2. 연관관계 없는 엔티티 외부 조인 ( 하이버네이트 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

     

    6. 서브 쿼리 

    ∙  나이가 평균보다 많은 회원


    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에서 불가능

    // 이런 From 서브쿼리 사용 불가
    String query = "select mm.age, mm.username " +
                        "from from (select m.age, m.useranme from Member m) as mm";
    

      →   그래서 1) 조인으로 풀 수 있으면 풀어서 해결해야 함😅(제일 좋은 방법)

                       2)From절 서브쿼리는 대부분 그 안에서 결과가 다 필터링이 되기 때문에 애플리케이션에서 직접 조작

                       3)쿼리를 2번 날려서 사용                          

                       4)위의 방법으로도 정 안되면 NativeSql을 사용 

     

    7. JPQL 타입 표현과 기타식

    타입 표현

    • 문자: ‘HELLO’, ‘She’’s’ 

        '를 표현하려면 ''두번 입력하면 된다.

    • 숫자: 10L(Long), 10D(Double), 10F(Float)

    • Boolean: TRUE, FALSE
    • ENUM: jpabook.MemberType.Admin (패키지명 포함)

    • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

     

    기타식

    • SQL과 문법이 같은 식

    • EXISTS, IN
    • AND, OR, NOT
    • =, >, >=, <, <=, <> BETWEEN, LIKE, IS NULL

     

     

    8. 조건식 (CASE 등등)

    기본 CASE 식

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

     

    String query = "select " +
                                "case when m.age <= 10 then '학생요금' " +
                                "     when m.age >= 60 then '경로요금' " +
                                "     else '일반요금' " +
                                "end " +
                        "from Member m";
    List<String> resultList = em.createQuery(query, String.class)
    				.getResultList();
    for(String s : resultList){
    	System.out.println("s = " + s);
    }

     

    단순 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

     

     

     

    9. JPQL 함수

    • CONCAT  : 문자열 합치기

     

    • SUBSTRING  : 문자열 빼기

     

    • TRIM  : 문자열 공백 제거

     

    • LOWER, UPPER  : 대 소문자 

     

    • LENGTH  : 문자열 길이

     

    • LOCATE  : 문자열 위치 

        select locate('de', 'abcdegf') From Member m;  → 4 출력 

     

    • ABS, SQRT, MOD  : 수학 Function (SQL과 동일)

     

    • SIZE, INDEX(JPA 용도)

       → SIZE는 컬렉션의 크기를 출력

       → INDEX는 List 값 타입일 때 컬렉션 위치 값 구할 때 사용 (권장x)

     

     

    사용자 정의 함수 호출 

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

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

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

     

    // DB방언 상속 (사용자 정의 함수 등록 - group_concat)
    public class MyH2Dialect extends H2Dialect {
    	public MyH2Dialect(){
        	resgisterFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
         }
    }
    
    // persistence.xml
    <property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
    
    
    // Main
    Member member1 = new Member();
    member1.setName("memberA");
    em.persist(member1);
    
    Member member2 = new Member();
    member2.setName("memberB");
    em.persist(member2);
    
    String query = "select function('group_concat', m.username) From Member m ";
    
    List<String> resultList = em.createQuery(query, String.class)
                        .getResultList();
    
    for(String s : resultList){
    	System.out.println("s = " + s);
        // 두 줄 출력되어여 함
        // s = memberA
        // s = memberB
    }
    
    // group_concat 사용으로 한 줄로 출력
    // s = memberA,memberB


    ❊ 출처

    인프런 강의 - 자바 ORM 표준 JPA 프로그래밍