JPQL Java Persistence Query Language )

JPQL은 SQL과 비슷한 문법을 가진 객체 지향 쿼리입니다.


JPQL의 탄생 배경은 JPA에서 제공하는 메서드 호출만으로 섬세한 쿼리 작성이 어렵다는 것에 있습니다.

이전 글 CURD에서는 SELECT 쿼리를 위해 JPQL을 사용했지만, EntityManager 객체의 find() 메서드를 호출하여 SELECT 쿼리를 수행 할 수도 있습니다.

Book book = em.find(Book.class, 1);

find() 메서드는 식별자를 통해서만 데이터 조회를 하며, 조건문도 없고 모든 칼럼을 조회하는 메서드입니다.

이것만 가지고는 조금이라도 복잡한 검색을 수행할 수가 없습니다.

따라서 여러 조건을 통해 검색을 하는 방법이 필요했고, 그래서 JPQL이 개발되었습니다.


JPQL 특징

1) 테이블이 아닌 객체를 검색하는 객체지향 쿼리

2) SQL을 추상화 했기 때문에 특정 벤더에 종속적이지 않음

3) JPA는 JPQL을 분석하여 SQL을 생성한 후 DB에서 조회





기본 문법

String jpql = "select c from Category c ";

JPQL은 SQL과 문법이 매우 유사하지만 몇 가지 다른 점이 있습니다.

1) 대소문자 구분

엔티티와 속성은 대소문자를 구분합니다.

예를 들어 엔티티 이름인 User, User 엔티티의 속성인 email은 대소문자를 구분합니다.

반면에 SELECT , FROM , AS 같은 JPQL 키워드는 대소문자를 구분하지 않습니다.


2) 엔티티 이름

위의 예제에서 select c from 뒤에 나오는 Category는 엔티티 이름입니다.

Category가 클래스 이름이라고 착각할 수 있는데, 그것이 아니라 @Entity( name="Category" )로 설정한 엔티티 이름입니다.

참고로 name 속성을 생략하면 기본 값으로 클래스 이름을 사용합니다.


3) 별칭

select c from Category c 에서 c라는 별칭을 주었습니다.

JPQL에서 엔티티의 별칭은 필수적으로 명시해야 합니다.

별칭을 명시하는 AS 키워드는 생략할 수 있습니다.



JPQL은 복잡한 검색을 위해 사용되기 때문에 INSERT , UPDATE , DELETE 쿼리는 엔티티 매니저가 직접 호출하도록 하는 것이 좋습니다.
그래서 이 글에서도 SELECT 쿼리에 초점을 맞춰 JPQL을 알아보도록 하겠습니다.




TypedQuery

public static void typedQuery(EntityManager em) {
	String jpql = "SELECT b FROM Book b ";
	TypedQuery<Book> query = em.createQuery(jpql, Book.class);
	
	List<Book> bookList = query.getResultList();
	for( Book book : bookList) {
		System.out.println(book.getTitle());
	}
}

모든 책 리스트를 조회하는 쿼리입니다.


EntityManager 객체에서 createQuery() 메서드를 호출하면 쿼리가 생성됩니다.

TypedQuery는 반환되는 엔티티가 정해져 있을 때 사용하는 타입이며,

em.createQuery 메서드를 호출할 때 두 번째 인자로 엔티티 클래스를 넘겨줍니다.


TypedQuery 객체의 getResultList() 메서드를 호출하면 작성한 JPQL에 의해 데이터를 검색하며, List 타입으로 반환합니다.





Query

public static void Query(EntityManager em) {
	String jpql = "SELECT b.no, b.title FROM Book b";
	Query query = em.createQuery(jpql);
	
	List<Object> list = query.getResultList();
	for( Object object : list ) {
	      Object[] results = (Object[]) object;
	      
	      for( Object result : results ) {
	          System.out.print ( result );
	     }
	     System.out.println();
	  }
}

TypedQuery와 달리 Query 타입은 데이터 검색 결과의 타입을 명시하지 않습니다.

그래서 List의 제네릭 타입으로 Object를 작성했습니다.


그리고 변수 jpql에서 SELECT 하는 칼럼을 선택적으로 명시한 점을 주목해주세요.

Query 타입을 사용하면 이런 식으로 여러 개의 칼럼을 선택적으로 명시할 수 있습니다.


public static void Query(EntityManager em) {
	String jpql = "SELECT b.no, b.title FROM Book b";
	TypedQuery<Book> query = em.createQuery(jpql, Book.class);
	
	List<Book> bookList = query.getResultList();
	for( Book book : bookList ) {
		System.out.println(book.getTitle());
	}
}

위와 같이 여러 개의 칼럼을 선택적으로 명시한 후, TypedQuery를 선언하면 에러가 발생합니다.





setParameter (1) - 이름 기준 파라미터 바인딩

public static void namedParameter(EntityManager em, String param1) {
	String jpql = "SELECT b FROM Book b WHERE title = :foo";
	TypedQuery<Book> query = em.createQuery(jpql, Book.class);
	query.setParameter("foo", param1);
	
	List<Book> bookList = query.getResultList();
	for( Book book : bookList) {
		System.out.println(book.getTitle());
	}
}

SELECT 쿼리를 수행할 때 항상 고정된 데이터를 조회하지 않을 수 있습니다.

예를들어 게시판에서 글 제목을 검색할 때 사용자가 검색하는 키워드는 유동적입니다.

따라서 동적으로 데이터가 바인딩 되기 위한 방법이 필요합니다.


위의 예제에서 사용한 방법은 이름을 기준으로 파라미터를 바인딩 하는 방법입니다.

콜론( : )을 사용하여 데이터가 추가될 곳을 지정해주고,

query.setParameter() 메서드를 호출하여 데이터를 동적으로 바인딩 합니다.





setParameter (2) - 위치 기준 파라미터 바인딩

public static void NamedParameter2(EntityManager em, String param1) {
	String jpql = "SELECT b FROM Book b WHERE title = ?1";
	TypedQuery<Book> query = em.createQuery(jpql, Book.class);
	query.setParameter(1, param1);
	
	List<Book> bookList = query.getResultList();
	for( Book book : bookList) {
		System.out.println(book.getTitle());
	}
}

이름 기준 파라미터 바인딩 방식과 유사하지만, 이름이 아닌 위치를 기준으로 바인딩이 이루어집니다.

또한 데이터가 추가될 곳을 지정하는 기호가 다른데, 위치 기준 방식에서는 물음표( ? )를 사용합니다.

setParameter() 메서드를 호출하는 방식은 같습니다.



두 가지 방식의 기능적 차이는 없으므로, 편한대로 사용하시면 됩니다.

단, 동적으로 추가되는 데이터는 꼭 바인딩을 해서 사용해야 합니다.

String jpql = "SELECT b FROM Book b WHERE title = " + param1;

이런 식으로 데이터를 직접 바인딩 할 경우, SQL Injection의 위험이 있으니 꼭 setParameter() 메서드를 호출하여 동적으로 데이터를 바인딩을 해야 합니다.





DTO를 사용할 경우 - new 명령어

DTO란 Data Transfer Object로서 데이터를 전송하는 객체입니다.

데이터를 전송하기 위해 엔티티를 사용하면 되지 않냐고 생각 할 수 있습니다.

실제로 그렇게 사용해오고 있지만, 만약 엔티티에 정의된 필드 외에 추가적인 필드가 필요할 경우, DB 테이블에서 정의하지 않은 칼럼을 엔티티에 임의로 추가하는 것은 바람직하지 않습니다.

그러면 엔티티와 테이블 매핑이 완전히 이루어진 것이 아니기 때문이죠.

그래서 사용하는 것이 DTO라는 객체입니다.


예를들어, Book , Category 엔티티가 있을 때 책 제목( title )과 카테고리 이름( categoryName )을 함께 전달해줘야 하는 경우에 사용합니다.

public class BookDTO {
	private String title;
	private String categoryName;
}



예제에서는 코드를 간단하게 하기 위해 BookDTO를 다음과 같이 정의하겠습니다.

public class BookDTO {
	private Integer no;
	private String title;
	
	public BookDTO() {}
	
	public BookDTO(Integer no, String title	) {
		this.no = no;
		this.title = title;
	}
	
	public Integer getNo() {
		return no;
	}
	public void setNo(Integer no) {
		this.no = no;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
}

Book 엔티티의 번호와 제목을 담고 있는 DTO입니다.

기본 생성자와 더불어, 생성자를 오버라이딩 한 또 다른 생성자가 있다는 점을 주목해주세요.



이제 DTO를 사용하는 방법에 대해 알아보겠습니다.

// DTO 사용 ( new 명령어 )
public static void useDTO (EntityManager em) {
	String jpql = "SELECT new com.victolee.example.dto.BookDTO(b.no, b.title) FROM Book b";
	TypedQuery<BookDTO> query = em.createQuery(jpql, BookDTO.class);
	
	List<BookDTO> list = query.getResultList();
	for( BookDTO dto : list) {
		System.out.println(dto.getTitle());
	}
}

JPQL이 조금 지저분합니다.

SELECTFROM 사이에 new라는 키워드로 BookDTO를 생성하는 것처럼 보입니다.

( new 키워드 뒤에 DTO의 패키지명까지 작성해야 한다는 것에 주의하세요. )

이 때 new는 객체를 생성하라는 의미가 아니라 JPQL에서 지원하는 new 키워드입니다.


BookDTO에서 생성자를 오버로딩한 이유는 위와 같이 JPQL을 작성하기 위함입니다.

즉 BookDTO 객체 필드 값으로 쿼리의 결과 값을 할당합니다.




유사 검색

String jpql = "select b from Board b where b.title LIKE CONCAT('%',:kwd,'%') ";



조인

// 방법1
String jpql = "SELECT b.title, c.name FROM Book b, Category c WHERE b.no = b.category.no";
// 방법2
String jpql = "SELECT b, c FROM Book b JOIN b.category c";

Query query = em.createQuery(jpql);

List<Object[]> list = query.getResultList();
for( Object[] row : list ) {
     Book book = (Book)row[0];
     Category category = (Category)row[1];
}

Book과 Category 엔티티가 관계를 맺고 있을 때, 조인을 하는 방법입니다.

아직 두 엔티티를 매핑하는 연관 관계 매핑에 대해 다루지 않았지만 JPQL을 다룰 때 한꺼번에 정리를 하려고 합니다.


서로 다른 두 엔티티를 조회 했으므로 TypedQuery 타입으로 반환할 수 없습니다.


방법2에서 주의할 것은 join 할 때 FROM Book b JOIN Category c 와 같이 SQL처럼 작성하면 안됩니다.


그런데 join 조회 결과를 캐스팅하여 사용하는 것이 번거로우므로 DTO를 사용하는 것이 훨씬 깔끔할 것 같습니다.

어쨋든 조인은 이런 식으로 사용할 수 있습니다.





이상으로 JPQL을 사용하는 기본적인 방법에 대해 알아보았습니다.

이 글에서 알아본 JPQL은 맛보기에 불과하며, 더 많은 정보를 원하시면 오라클 문서를 참고하시면 좋을 것 같습니다. ( 링크 )


다음 글에서는 SQL스러운 JPQL 조차 사용하지 않고, 진정한 의미인 메서드 호출만으로 쿼리를 수행하는 QueryDSL을 알아보도록 하겠습니다.


댓글 펼치기 👇
  1. tylor 2019.04.09 09:21

    좋은 글 감사합니다, 많은 도움이 되네요 :)