[JPA] JPQL - 기본

3Beom's 개발 블로그·2023년 6월 18일
0

SpringJPA

목록 보기
14/21
post-custom-banner

출처

본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 수강하며 기록한 필기 내용을 정리한 글입니다.

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


JPA에서 제공하는 다양한 쿼리 방법

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

JPQL

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

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

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

⭐ JPQL은 엔티티 객체를 대상으로 쿼리를 작성함

⭐ SQL은 DB 테이블을 대상으로 쿼리를 작성함

JPQL 예시와 문제점

String jpql = "select m from Member m where m.username like '%kim%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
System.out.println(result);
  • 위와 같이 문자열로 JPQL 문법에 맞춰서 쿼리를 만들고, em.createQuery() 메서드를 활용함.
  • 하지만 문자열로 쿼리를 만들어야 하기 때문에 동적쿼리를 만들기 어려움.
  • 위 쿼리의 경우, '%kim%' 부분에 변수가 들어가야함
  • 그럼 문자열 붙이기 과정이 필요하고, 여러모로 불편한 점이 있다.
  • 이를 보완하기 위한 방안이 다음 두가지가 있다.
    • JPA Criteria
    • QueryDSL

JPA Criteria 예시와 문제점, QueryDSL

			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> result = em.createQuery(cq).getResultList();
  • JPQL의 빌더 역할을 함.
  • JPA 공식 기능임.
  • 문자열이 아닌 자바 코드로 JPQL을 작성할 수 있어 컴파일 오류를 막을 수 있다.
  • 하지만 딱 봐도 복잡하고 어렵다. : 유지보수가 어려움
  • 실무에서는 잘 안쓴다.
  • 너무 복잡하다.
  • Criteria 대신 QueryDSL이 더 직관적이고 간편하다.

→ QueryDSL 활용 예

  • JPQL 문법을 숙지하고 있으면 QueryDSL 활용이 용이하다.

SQL 그대로 활용하기 (Native Query)

List<Member> result = em.createNativeQuery("select * from member m where m.username like '%kim%'").getResultList();
  • 위와 같이 em.createNativeQuery() 메서드를 활용하여 파라미터로 SQL을 전달하면 해당 SQL이 DB에 그대로 보내진다.

JDBC, MyBatis, SpringJdbcTemplate 등

  • 위 방식대로도 DB와 소통할 수 있다.
  • JPQL, QueryDSL, NativeQuery 등의 방식들은 모두 EntityManager em 내에 정의된 메서드들(em.createQuery(), em.createNativeQuery())을 활용하기 때문에 해당 쿼리들을 전송하기 전에 자동으로 flush()가 된다.
  • 하지만 Entity Manager를 활용하지 않는 방식들(JDBC, MyBatis, SpringJdbcTemplate)은 JPA와 전혀 관계 없는 독립적인 DB 커넥션 기능들이기 때문에 적절한 시점에 flush()를 해줘야 한다.
  • 본 기능들을 활용할 때는 JPA 영속성 컨텍스트 원리를 잘 고려해서 적절한 시점에 flush로 DB에 강제로 반영하는 등의 과정이 필요할 수 있다.

JPQL 기본 문법

  • 기본적으로 SQL과 동일함

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

  • Member : Member 엔티티를 의미함. 테이블이 아님에 유의.
  • 엔티티와 속성은 대소문자를 구분함. (Member, age)
  • JPQL 키워드는 대소문자 구분 안함 (select, from, where)
  • 별칭(alias)는 필수임. (as는 생략 가능)
    • Member as m, Member m

집합, 정렬

  • 기본적으로 다 지원됨

+Group By, Having, Order By 다 지원됨

Query, TypedQuery

  • TypedQuery : JPQL 쿼리의 반환 타입이 명확할 때.
    TypedQuery<Member> typedQuery = em.createQuery("select m from Member m where m.age > 18", Member.class);
    TypedQuery<String> typedQuery2 = em.createQuery("select m.username from Member m where m.age > 18", String.class);
  • Query : JPQL 쿼리의 반환 타입이 명확하지 않을 때. 여러 타입이 섞여 있을 때
    Query query = em.createQuery("select m.username, m.age from Member m where m.age > 18");

결과 조회 API

  • 결과가 하나 이상일 때 → getResultList()
    TypedQuery<Member> typedQuery = em.createQuery("select m from Member m where m.age > 18", Member.class);
    List<Member> members = typedQuery.getResultList();
    • getResultList() 메서드의 경우, 결과가 없으면 빈 리스트를 반환해준다. → NullPointerException 걱정 안해도 된다.
  • 결과가 하나일 때 → getSingleResult() : 결과가 정확히 하나만 나와야함
    TypedQuery<Member> typedQuery = em.createQuery("select m from Member m where m.age > 18", Member.class);
    Member member = typedQuery.getSingleResult();
    • 결과가 올바르지 않을 때(결과가 없거나, 둘 이상이면) 예외를 발생한다.

    • 결과 없으면 : javax.persistence.NoResultException

    • 결과 둘 이상이면 : javax.persistence.NonUniqueResultException

      → 따라서 해당 메서드를 쓸 때는 try-catch 로 처리해줄 필요가 있다.

파라미터 바인딩

  • 이름 기준, 위치 기준 방식이 있다.
  • 이름 기준
    • Vue.js처럼 : 로 바인딩해준다.

      String usernameParam = "UserName1";
      TypedQuery<Member> bindingQuery = em.createQuery("select m from Member m where m.username = :username", Member.class);
      bindingQuery.setParameter("username", usernameParam);
  • 위치 기준
    • PreparedStatements 에서 setInt, setString 했던 것 처럼 위치를 기준으로 설정한다.

      String usernameParam = "UserName1";
      TypedQuery<Member> bindingQuery = em.createQuery("select m from Member m where m.username = ?1", Member.class);
      bindingQuery.setParameter(1, usernameParam);

⇒ 하지만 웬만하면 이름 기준으로 하는게 좋음. 더 직관적이다.

프로젝션

  • SELECT 절에 조회할 대상 지정하는 것
  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터타입)
  • SELECT m FROM Member m : 엔티티
  • SELECT [m.team](http://m.team) FROM Member m : 엔티티 프로젝션
    • team도 Team 엔티티이므로 엔티티 프로젝션이다.
    • 하지만 JPQL로 이렇게 쓰기보다 다음과 같이 join으로 하는게 좋다.
    • select t from Member m join [m.team](http://m.team) t
  • SELECT m.address FROM Member m : Address 라는 임베디드 값 타입을 Member 엔티티에서 갖고 있으므로 임베디드 타입으로 설정된다.
  • SELECT m.username, m.age FROM Member m : 스칼라 타입 프로젝션
  • DISTINCT 로 중복 제거도 가능하다.

⇒ 엔티티 프로젝션의 경우, 결과로 반환된 엔티티 객체들은 영속성 컨텍스트에 의해 관리된다.

프로젝션 - 여러 값 조회

  • SELECT m.username, m.age FROM Member m 이렇게 여러 타입의 값들이 반환될 경우, 각 값은 다음 방식들로 가져올 수 있다.
    • Query 타입으로 조회
    • Object[] 타입으로 조회
    • new 명령어로 조회
  • 다음과 같이 받아올 수 있다.
    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]);
    • 위 방식을 다음과 같이 List의 Generic에 Object[]를 지정함으로써 간략히 할 수도 있다.

      List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
                                  .getResultList();
      
      Object[] result = resultList.get(0);
      System.out.println("username : " + result[0]);
      System.out.println("age : " + result[1]);
    • 하지만 본 방식들은 불편하다.

  • new 명령어로 조회하는게 가장 깔끔하다.
    • DTO 파일을 따로 생성해서 활용하는 방식이다.

    • <MemberDTO 생성>

      package jpql;
      
      public class MemberDTO {
          private String username;
          private int age;
      
          public MemberDTO(String username, int age) {
              this.username = username;
              this.age = age;
          }
      
          public String getUsername() {
              return username;
          }
      
          public void setUsername(String username) {
              this.username = username;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      }
    • 다음과 같이 활용한다.

      List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
                          .getResultList();
      MemberDTO memberDTO = result.get(0);
      System.out.println("username : " + memberDTO.getUsername());
      System.out.println("age : " + memberDTO.getAge());
    • 위 코드를 보면 new jpql.MemberDTO(m.username, m.age) 부분에서 MemberDTO 객체를 생성해서 반환하는 것이다.

    • 이에 따라 적절한 생성자가 꼭 필요하다.

      • 위 예시의 경우, username, age를 파라미터로 받는 생성자 필요!
      • 순서와 타입 모두 생성자와 일치해야한다.
    • new 이후에는 패키지 명을 전부 다 써줘야 한다.

      • 이는 단점이다. 하지만 이후 QueryDSL 등에서 극복 가능

페이징 API

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

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

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

            for (Member m : result) {
                System.out.println(m);
            }

            tx.commit();
  • 위와 같이 시작 데이터 인덱스 설정하고, 개수만 설정해주면 이에 맞춰서 각 DB 형식에 맞춰서 SQL을 JPA가 알아서 짜준다.

JPQL 타입 표현

  • 문자 : ‘HELLO’, ‘She’’s’
  • 숫자 : 10L(Long), 10D(Double), 10F(Float)
  • Boolean : TRUE, FALSE
  • ENUM : jpabook.MemberType.Admin (패키지명 포함해야함) 혹은 파라미터 바인딩으로 넘겨줘도 된다.
    em.createQuery("select m from Member m where m.type=:userType")
    			.setParameter("userType", MemberType.Admin)
    			.getResultList();
  • 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용함)
    • 자식 엔티티들 중 특정 타입의 엔티티에 대한 데이터만 조회하고 싶을 경우

      em.createQuery("select i from Item i where type(i) = Book", Item.class)
      			.getResultList();

      → 상속관계 DB에서 부모 테이블의 DTYPE 컬럼에 where 절이 설정됨

JPQL 기본 함수

  • 다음은 JPQL에서 제공하는 표준 함수들이다.
    • concat
    • substring
    • trim
    • lower, upper
    • length
    • locate : 문자열 내에서 특정 부분 문자열의 위치 찾기
    • abs, sqrt, mod
    • size, index(JPA 용도)
  • 이 외에도 각 DB마다 서로 다르게 제공되는 함수들을 모두 쓸 수 있게 등록되어있다.
    • 하지만 만약 DBMS가 바뀌면 그 DBMS에 맞는 함수로 고쳐야함.

JPQL 사용자 정의 함수

  • 쓰고자 하는 DB의 방언을 상속받은 클래스 내에서 정의해주어야 한다.
public class MyH2Dialect extends H2Dialect {
		public MyH2Dialect() {
				registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
		}
}

→ H2 DB 방언을 상속받아 사용자 정의 함수 group_concat 을 등록한 것.

  • 이후 persistence.xml에서 hibernate.dialect 를 상속받은 MyH2Dialect 로 설정해주면 된다.
    • <property name="hibernate.dialect" value="dialect.MyH2Dialect" />
  • JPQL에서 활용하면 된다.
    SELECT FUNCTION('group_concat', m.username) FROM Member m
    → FUNCTION() 을 쓰면 됨

JPQL 기타

  • 웬만하면 SQL과 기본적인 문법은 모두 같다.
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN AND, LIKE, IS NULL, CASE WHEN THEN, COALESCE, NULLIF 모두 가능
profile
경험과 기록으로 성장하기
post-custom-banner

0개의 댓글