JAVA/JPA 학습기록

[JPA] JPA에서 SQL 쿼리를 작성하는 방법_ JPQL

kth990303 2021. 6. 9. 20:01
반응형

김영한님의 스프링 MVC 1편 강좌를 수강하기 전에,

JPA 기본편은 완강하고 들으려고 열심히 수강하고 있다.

블로그에 가볍게 기록해보려 한다.

 

출처:  자바 ORM 표준 JPA 프로그래밍 - 기본편 (인프런 김영한 강사님)


JPA를 다루는데 SQL문을 알아야 하는 이유?

JPA는 Entity 객체 단위로 개발하기 때문에 객체지향적 설계에 효과적이다.

조회, Create, Delete와 같은 단순 쿼리 작업 시에 굉장히 간단하고 효율적이다. em.persist, em.remove와 같은 메소드를 제공해주기 때문이다.

JPA는 검색 시에도 테이블이 아닌 엔티티 객체를 대상으로 검색한다.

그러나 모든 데이터를 엔티티 객체로 변환하여 검색하는 것은 성능 이슈 등 여러가지 문제점으로 인해 불가능하므로 결국 Native SQL문을 배워서 쓸 일이 있기 때문에 우리는 SQL문 또한 충실히 배워두어야 한다.

 

JPA와 함께 SQL문을 사용하는 다양한 방법이 있다.

SQL을 추상화한 객체지향 쿼리 언어 JPQL,

SQL, JPQL을 코드로 작성하여 컴파일 시점에 문제를 확인할 수 있으며, 동적 쿼리를 다룰 수 있는 QueryDSL이 있다.

 

이번 시간엔 JPQL을 살펴보려 한다.


JPQL 사용

JPQL은 객체지향 쿼리 언어이므로 테이블 대상이 아닌, 엔티티 객체를 대상으로 쿼리를 날린다.

회원 객체 중 나이가 18세를 넘는 회원들만 조회하고 싶을 때, 아래와 같이 작성하면 된다.

 String jpql = "select m from Member m where m.age > 18";
 List<Member> result = em.createQuery(jpql, Member.class)
 	.getResultList();

SQL문과 거의 동일하나, select 옆에 m이 있다는 점, Member 옆에 m이 있다는 점이 특이하다.

이 때, Member는 테이블명이 아닌 엔티티 객체 이름이며, 별칭 m은 필수여서 m을 넣어준 것이다.

select 옆의 m은 조회 대상으로 프로젝션이라고 불리는데, 아래에 서술할 내용이다.

또한, em.createQuery(jpql, Member.class) 에서 두 번째 인자에 해당하는 부분은 조회하려는 변수의 자료형 클래스를 입력해주면 된다. 

 

createQuery 메소드의 객체 자료형은 아래와 같다.

TypedQuery<T>와 Query형 두 가지로 나누어진다. 차이점은 아래 코드를 보면서 이해할 수 있을 것이다.

// 반환타입(조회하려는 타입)이 Member임이 자명함.
TypedQuery<Member> query =
 em.createQuery("SELECT m FROM Member m", Member.class);
 
 // 반환타입이 String, Integer 두 개 이상으로 명확하지 않음.
 Query query =
 em.createQuery("SELECT m.username, m.age from Member m");

결과 조회 API

  • getResultList(): 결과가 하나 이상일 때, 리스트 반환. 결과가 없으면 빈 리스트를 반환한다.
  • getSingleResult(): 결과가 정확히 하나일 때 단일 객체를 반환. 결과가 없을 경우 NoResultException, 결과가 둘 이상이면 NonUniqueResultException을 터뜨린다.

실제로 getSingleResult()의 런타임에러 경우 때문에 try-catch문으로 따로 예외처리를 하거나 getResultList를 많이 쓴다고 한다.


파라미터 바인딩

객체 프로퍼티 값을 파라미터로 직접 설정해주는 방법을 파라미터 바인딩이라고 한다.

아래 코드는 Member 객체의 인스턴스를 생성하여 kth990303이라는 멤버를 만들어주고 그 쿼리 결과를 반환해주는 코드이다.

TypedQuery<Member> query =
 em.createQuery("SELECT m FROM Member m WHERE m.name= :name", Member.class)
 .setParameter("name", "kth990303");

List<Member> list = query.getResultList();

property 앞에 :를 붙여서 바인딩해주면 된다.

 

위 코드는 아래와 같이 위치 기준 파라미터 바인딩으로 할 수도 있다.

TypedQuery<Member> query =
 em.createQuery("SELECT m FROM Member m WHERE m.name= ?1", Member.class)
 .setParameter(1, "kth990303");

List<Member> list = query.getResultList();

? 다음 값에 위치를 주면 된다.

위치 기준보단 이름 기준의 바인딩을 추천한다고 하는데, 요소가 추가되거나 삭제될 때 위치가 변할 수 있기 때문이다.

 

파라미터 바인딩은 매우 효율적이다. 파라미터만 다르고 결과적으로 같은 쿼리로 인식하여 재사용이 가능하다는 효율성, 그리고 직접 문자열을 입력할 때 SQL Injection을 당할 수 있음을 방지해주는 보안성이 지켜지기 때문이다.


프로젝션

select 절에 조회할 대상을 지정하는 것을 의미한다.

엔티티 객체, 임베디드 타입, 스칼라 타입이 프로젝션 대상이 될 수 있다.

// 엔티티 프로젝션
SELECT m FROM Member m
SELECT m.team FROM Member m	// 묵시적 조인 쿼리 -> 성능 이슈 한눈에 파악 어려움 -> JPQL에 조인을 명시해주자

// 임베디드 타입 프로젝션
SELECT m.address FROM Member m

// 스칼라 타입(String, Integer 등 기본 데이터 타입 프로젝션)
// 스칼라 타입은 DISTINCT로 중복 제거 가능
SELECT m.username, m.age FROM Member m
SELECT DISTINCT m.username FROM Member m

위와 같이 작성하여 조회한 엔티티는 영속성 컨텍스트에서 관리되나, 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

String query = "SELECT m.address FROM Member m"; 

List<Address> addressList = em.createQuery(query, Address.class)
                             .getResultList();

위 코드는 Member 객체의 Address (임베디드 타입)을 조회하는 쿼리이다. 따라서 두 번째 인자로 Address.class가 들어간다.

 

// 여러 스칼라 타입 값을 조회하는 쿼리
Query query = em.createQuery("SELECT m.username, m.age, m.team FROM Member m");
List<Object[]> resultList = query.getResultList();

for(Object[] row : resultList){
    String username = (String)row[0];
    Integer age = (Integer)row[1];
    Team team = (Team)row[2];
}

위 코드는 username, age 두 개 이상의 스칼라 타입을 조회하는 쿼리여서 Query 자료형으로 반환한다.

따라서 자료형이 명확하지 않으므로 Object[]를 반환하는데 위와 같이 프로퍼티마다 자료형을 맞춰야된다는 불편함이 있다.

 

그냥 객체를 바로 반환하는 방법은 없을까?

new 명령어를 통해 Object[] 대신 바로 객체로 생성해서 볼 수 있다.

TypedQuery<UserDTO> query = 
	em.createQuery("SELECT new jpa.jpql.UserDTO(m.username, m.age) FROM Member m". UserDTO.class); 

List<UserDTO> userDTOs = query.getResultList();

QueryDSL을 사용하면 전체 클래스명을 작성해야 하는 불편함을 생략할 수 있다고 한다.


페이징 API

성능 이슈 등의 문제로 페이지네이션 기법을 사용하고 싶은가? 그런데 Oracle과 같은 데이터베이스를 사용하여 페이지 처리가 불편한가? JPA에선 엄청 편리한 API를 제공해준다.

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
TypedQuery<Member> query = 
    em.createQuery("SELECT m FROM Member m ORDER BY m.age DESC", Member.class);

query.setFirstResult(20);
query.setMaxResult(20);
query.getResultList();

위와 같이 작성하면 나이가 많은 순부터 21~40번째 데이터를 조회하는 것이다.

지원하는 모든 데이터베이스를 추상화한 것이므로 db를 바꾼다 하더라도 설정만 바꿔주면 된다는 점이다.


오랜만에 블로그에 포스팅을 해보았다.

이번 포스팅은 다른 블로그 포스팅 또한 많이 참고해보았는데, 여러 블로그들을 참고하니까 확실히 놓친 부분들 또한 꼼꼼하게 점검할 수 있어 좋은듯하다.

 

참고한 블로그

https://joont92.github.io/jpa/JPQL/

 

[jpa] JPQL

JPA에서 현재까지 사용했던 검색은 아래와 같다. 식별자로 조회 EntityManager.find() 객체 그래프 탐색 e.g. a.getB().getC() 하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다. 그

joont92.github.io

https://ykh6242.tistory.com/91

 

자바 ORM 표준 JPA 프로그래밍(12) - JPQL 기본 문법과 파라미터 바인딩, 프로젝션, 페이징

JPQL(Java Persistence Query : Language) JPQL은 객체지향 쿼리 언어이다. 테이블 대상으로 쿼리하는 것이 아닌 엔티티 객체를 대상으로 쿼리한다. JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 종속적이

ykh6242.tistory.com

https://ict-nroo.tistory.com/116

 

[JPA] 객체지향 쿼리, JPQL

JPA 객체지향 쿼리 연관관계 매핑과 영속성 컨텍스트 등 앞의 내용들로 기본기를 다졌고, 지금부터는 활용 단계이다. JPA와 객체지향 쿼리 QueryDSL JPA는 다양한 쿼리 방법을 지원 JPQL JPA Criteria QuertD

ict-nroo.tistory.com

 

반응형