|
JPQL(Java Persistence Query Language) 쿼리 구성과 함정
Java Persistence API는 사용자가 엔티티 및 해당 퍼시스턴스 상태에 대한 쿼리를 정의할 수 있게 해주는 질의어를 지정한다. 또한 JPQL(Java Persistence Query Language)이라 불리는 이 질의어는 엔터프라이즈 환경에서 사용하는 특정 데이터베이스와는 별도로 이식 가능한 방법으로 쿼리의 의미를 지정할 수 있게 해준다.
위의 테크팁에서 Java Persistence Query Language를 소개하고 기본 기능에 관해 논한 바 있으며, 이번 호 테크 팁에서는 JPQL 쿼리 구성 시 주의해야 할 사항들에 대해 알아보도록 한다. 본 테크팁은 독자가 Java Persistence API의 기본 용어와 개념을 이미 이해하고 있는 것으로 가정하여 진행한다. 그렇지 않은 경우에는 Java EE 5 Tutorial의 "Chapter 24: Introduction to the Java Persistence API(영문)"의 내용을 참조하기 바란다.
본 팁에는 예제 패키지가 첨부되어 있으며, 팁의 코드 예제들은 패키지에 포함되어 있는 예제 소스 코드에서 발췌한 것이다. 예제에는 Java EE 5 SDK를 사용했으며, Java EE 5 SDK는 Java EE 다운로드 페이지에서 다운로드할 수 있다.
http://java.sun.com/javaee/downloads/index.jsp
본 팁에 사용된 엔티티
먼저 두 가지 퍼시스턴스 엔티티를 살펴보도록 하자. 이 엔티티는 "Introduction to the Java Persistence Query Language" 팁에서 사용된 2개의 엔티티와 동일한 Customer과 Order이며, One-to-Many(일 대 다) 관계를 가진다. Customer 엔티티 클래스와 Order 엔티티 클래스는 다음과 같이 정의된다.
@Entity
@Table(name=&qout;CUSTOMER_TABLE&qout;)
public class Customer implements Serializable{
public enum CustomerStatus
{FULL_TIME, PART_TIME, CONTRACT};
@Id
@Column(name="ID")
private Integer customerId;
@Column(name="CITY")
private String city;
@Column(name="NAME")
private String name;
@Enumerated(ORDINAL)
@Column(name="STATUS")
private CustomerStatus status;
@OneToMany(mappedBy="customer")
private Collection orders;
...
}
@Entity
@Table(name="ORDER_TABLE")
public class Order implements Serializable {
@Id
@Column(name="ID")
private Integer orderId;
@Column(name="quantity")
private int quantity;
@Column(name="totalPrice")
private float totalPrice;
@ManyToOne()
@JoinColumn(name="CUST_ID")
private Customer customer;
...
}
다음은 이들 엔티티의 인스턴스를 생성하고, 해당 퍼시스턴스 관리를 위한 엔티티 매니저를 생성하고, 인스턴스를 데이터베이스에 삽입하는 클라이언트 코드이다.
// Create an EntityManagerFactory for a persistence unit
// called j2seEnvironment.
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("j2seEnvironment");
// Create an Entity Manager
EntityManager em = emf.createEntityManager();
// get a Transaction
EntityTransaction tx = em.getTransaction();
// create a POJO instance of the Customer class
Customer customer = new Customer();
customer.setCustomerId(new Integer(3));
customer.setName("SUN_SALE");
customer.setCity("SAN JOSE");
customer.setStatus(Customer.CustomerStatus.FULL_TIME);
// create a POJO instance of the Order class
// for this customer
Order order = new Order();
order.setOrderId(new Integer(3));
order.setQuantity(new Integer(2));
order.setTotalPrice(new Float(22.30));
order.setCustomer(customer);
// Make the Customer and Order instances persistent
// and insert them into the database
tx.begin();
em.persist(customer);
em.persist(order);
tx.commit();
이제 개발자들에게 종종 문제가 되는 몇 가지 JPQL 쿼리를 살펴보고, 이를 엔티티에 대해 실행해보도록 하자.
탈출 문자(Escape Character)를 가지는 LIKE 표현식 사용하기
JPQL은 LIKE 표현식을 포함한 다양한 종류의 비교 표현식을 지원한다. 하지만 표현식이 백슬래시(\)를 탈출 문자로 지정할 경우에는 JPQL 쿼리에서 LIKE 표현식을 사용하기가 까다로울 수 있다.
LIKE 표현식은 문자열이 패턴과 일치하는지 여부를 결정하며, 표현식 구문은 다음과 같은 형태를 띤다.
string_expression LIKE pattern_value [ESCAPE escape_character] string_expression은 반드시 문자열 값을 포함하는데, 이는 사용자가 일치시키고자 하는 문자열이다. pattern_value는 문자열 리터럴 또는 string-valued 인풋 파라미터로, 이는 문자열을 일치시키기 위한 일종의 패턴으로 보면 된다. pattern_value에는 하나의 문자를 나타내는 밑줄 문자(_)와 연속된 문자를 나타내는 퍼센트 문자(%)가 포함될 수 있다. 옵션사항인 탈출 문자는 단일 문자열 리터럴이나 문자값 인풋 파라미터이며 pattern_value에서 밑줄 및 퍼센트 문자의 특수 의미를 제거하는 데 사용된다.
다음의 LIKE 표현식을 예로 들 수 있다.
customer.name LIKE 'f_r%' 이 표현식의 경우 customer.name의 값을, 첫 번째 문자가 "f", 두 번째 문자는 임의의 문자, 세 번째 문자는 "r"인(임의 수의 문자가 뒤따르는) 패턴과 비교한다. 이 때, customer.name의 값이 "fore" 또는 "forego"이면 표현식은 참이고 "four" 또는 "f_rst"이면 참이 아니다.
다음의 LIKE 표현식
customer.name LIKE 'f\_r%' ESCAPE '\' 은 customer.name의 값을, 첫 번째 문자는 "f", 두 번째 문자는 밑줄 문자(백슬래시는 밑줄 문자의 특수 의미를 제거하기 위해 사용되었다), 세 번째 문자는 "r"이고, 그 뒤로 임의 수의 문자가 이어지는 패턴과 비교한다. 이 때, customer.name의 값이 "f_r" 또는 "f_rst"이면 표현식은 참이고 "fore" 또는 "forego"이면 참이 아니다.
백슬래시 문자를 탈출 문자로 지정하는 LIKE 표현식은 사용자가 두 번째 백슬래시를 지정하는 경우에만 올바르게 작동한다. 왜냐하면 자바 컴파일러에서도 백슬래시 문자를 탈출 문자로 취급하기 때문이다. 관련 예제는 다음과 같다.
// run a Java Persistence query using input parameters
String ejbql = "SELECT c from Customer c
WHERE c.name LIKE :pattern ESCAPE :esc";
Query query = em.createQuery(ejbql);
query.setParameter("pattern", "\\_%");
query.setParameter("esc", '\\');
보다시피 setParameter() 메소드 호출에 두 개의 백슬래시가 사용되고 있으며, 각 메소드 호출의 첫 번째 백슬래시는 자바 컴파일러에 의해, 그리고 두 번째 백슬래시는 JPQL 컴파일러에 의해 처리된다. 그 결과, 첫 번째 문자가 밑줄 문자인 문자열이 일치하게 된다.
다음은 인풋 파라미터를 사용하지 않는 동일한 코드의 예이다.
String ejbql = "SELECT i FROM Item i
WHERE i.name LIKE '\\_%' ESCAPE '\\'";
Query query = em.createQuery(ejbql);
Enumerated 주석 사용하기
프로그래머들은 종종 JPQL과 함께 Enumerated 주석을 사용할 때 문제에 부딪히곤 한다.
Enumerated 주석은 퍼시스턴트 속성이나 필드를 Enumerated Type으로 퍼시스트할 것을 지정하고, enum(모든 Enumerated Type을 위한 기본 클래스)은 문자열이나 정수로 매핑될 수 있다. Enumerated Type에는 ORDINAL과 STRING의 두 가지가 있는데, Enumerated Type이 지정되지 않을 경우 Enumerated Type은 ORDINAL로 간주된다.
enum Type을 기초로 숫자나 문자열 값을 할당하고자 할 때 문제가 발생하게 된다. 예를 들어, 엔티티 클래스 Customer에서는 CustomerStatus가 enum이고, CustomerStatus로 정의되는 Customer의 필드 상태는 정수로 매핑된다. 한편, 여러분은 아래의 JPQL 쿼리가 고객 상태를 올바르게 질의하고 있다고 여길 것이다.
String ejbql = "SELECT c FROM Customer c
WHERE c.status = 1";
사실 이 쿼리는 java.lang.IllegalArgumentException을 throw하는데, 왜냐하면 JPQL 컴파일러가 enum 상수를 예상하고 있기 때문에 예외가 throw되는 것이다.
고객 상태를 질의하는 올바른 방법은 다음과 같다.
// run a Java Persistence query
String ejbql = "SELECT c FROM Customer c
WHERE c.status = :status";
Query query = em.createQuery(ejbql);
query.setParameter(
"status", Customer.CustomerStatus.FULL_TIME);
올바른 쿼리에서는 enum 필드 status에 enum 상수 Customer.CustomerStatus.FULL_TIME이 할당된다.
조건 표현식에서 IN 연산자 사용하기
조건 표현식에서 IN 연산자는 값이 항목 또는 항목 세트 내에 있는지를 결정하고, NOT IN 연산자는 값이 항목 또는 항목 세트 내에 있지 않은지 여부를 결정한다.
조건 표현식의 IN 또는 NOT IN 비교 연산자를 위한 정형 구문(formal syntax)은 다음과 같다.
in_expression ::= state_field_path_expression [NOT] IN
(in_item {, in_item}* | subquery)
in_item ::= literal | input_parameter
state_field_path_expression은 비교를 위한 값을 포함하는 표현식이고, in_item은 값을 확인하기 위한 항목 또는 항목 세트이다.
예를 들어, 표현식 o.country IN ('UK', 'US', 'FRANCE')는 o.country의 값이 UK이면 참이고 o.country의 값이 PERU이면 거짓이다. 또한 표현식 o.country IN ('UK', 'US', 'FRANCE')는 o.country의 값이 UK이면 참이고 o.country의 값이 PERU이면 거짓이다.
조건 표현식에서 IN 또는 NOT IN 연산자를 사용할 때는 다음 사항을 염두에 둘 필요가 있다.
- state_field_path_expression은 반드시 문자열, 숫자 또는 enum 값을 가진다. 다른 Type은 지원되지 않는다.
- in_item 내의 항목 또는 항목 세트는 반드시 state_field_path_expression과 동일한 Type을 가진다.
- 서브쿼리의 결과는 반드시 state_field_path_expression과 동일한 Type을 가진다.
관련 예는 다음과 같다.
// IN with subquery
String ejbql = "SELECT o FROM Order o
WHERE o.customer.name
IN (SELECT c.name
FROM Customer c WHERE c.customerId = 3)";
Query query = em.createQuery(ejbql);
// IN with set of integers
String ejbql = "SELECT o FROM Order o
WHERE o.customer.name IN (2, 3)";
Query query = em.createQuery(ejbql);
// IN with set of strings
String ejbql = "SELECT o FROM Order o
WHERE o.customer.name IN ('foo', 'JIE_LENG')";
Query query = em.createQuery(ejbql);
네이티브 쿼리 사용하기
JPQL은 네이티브 쿼리를 지원하는데, 다시 말해 타깃 데이터베이스의 SQL을 이용하여 JPQL 쿼리를 표현할 수 있다. 한편, 네이티브 쿼리의 경우 데이터베이스간 이식성은 보장되지 않는다.
네이티브 쿼리의 결과는 엔티티, 스칼라 값 또는 엔티티와 스칼라 값의 조합으로 구성될 수 있다.
네이티브 쿼리가 복수의 엔티티 타입을 반환할 경우, 엔티티들은 반드시 @SqlResultSetMapping 주석 내 SQL 문의 칼럼 결과에 지정되고 매핑되어야 한다. 이 경우, 결과 세트 매핑 메타데이터는 퍼시스턴스 런타임이 JDBC 결과를 예상 오브젝트에 매핑하는 데 사용될 수 있다.
다음은 네이티브 SQL 쿼리가 단일 엔티티 클래스의 엔티티를 반환하는 예로, 결과 타입을 지정하는 엔티티 클래스는 하나의 인수로서 패스된다. 네이티브 쿼리는 단일 엔티티 타입을 반환하기 때문에 @SqlResultSetMapping 주석을 필요로 하지 않는다. 이 쿼리를 실행하면 "SUN_SALE"이라는 이름의 고객을 위한 모든 Order 엔티티의 Collection을 반환하게 된다.
Query q = em.createNativeQuery(
"SELECT o.id, o.quantity, o.customer " +
"FROM Order o, Customer c " +
"WHERE (o.customer = c.customerId)
AND (c.name = 'SUN_SALE')",
ejbql.models.Order.class);
List orders = q.getResultList();
다음 쿼리는 복수의 엔티티 타입을 반환하므로 @SqlResultSetMapping 주석을 필요로 하며, 이 예에서는 기본값 메타데이터와 칼럼 이름이 사용되었다고 가정한다. 단, @SqlResultSetMapping 주석은 항상 엔티티 클래스 정의에 앞서 지정되어야 한다는 점에 유의할 것.
@SqlResultSetMapping(name="OrderCustomerResults",
entities={ @EntityResult(
entityClass=ejbql.models.Order.class),
@EntityResult(entityClass=ejbql.models.Customer.class) } )
@Entity public class Order { ... }
다음 코드는 네이티브 쿼리를 실행한다.
Query q = em.createNativeQuery(
"SELECT o.id, o.quantity, c.customerId, c.name, c.city " +
"FROM Order o, Customer c " +
"WHERE (o.quantity > 25)
AND (o.customer = c.customerId)",
"OrderCustomerResults");
List result = q.getResultList();
int s = result.size();
for(int i = 0; i < s; i++){
Object obj = result.get(i);
Object[] objectArray = (Object[]) obj;
Object object1 = objectArray[0];
Object object2 = objectArray[1];
Order order = (Order) object1;
Customer customer = (Customer)object2;
}
NULL 값 사용하기
JPQL 쿼리에서 NULL 값을 사용할 때는 반드시 다음 사항에 유의해야 한다.
-'IS NULL'과 '= NULL'은 서로 다른 의미를 가진다.
- NULL 값을 이용한 비교 또는 산술 연산은 항상 미지의 값을 산출한다.
- 2개의 NULL 값은 동등하게 간주되지 않으며, 비교 시 미지의 값이 산출된다.
따라서 다음의 쿼리는
String ejbql = "SELECT c FROM Customer c
WHERE c.name = NULL";
Query query = em.createQuery(ejbql);
이름이 없는 고객이 있더라도 고객을 전혀 반환하지 않는다. 그 이유는 이름이 null인 경우 표현식 customer.name = NULL은 참이 아닌 미지의 값으로 평가되기 때문이다. 이는 값이 null로 설정된 인풋 파라미터를 이용하는 쿼리의 경우에도 마찬가지로 해당된다.
singled-valued 경로 표현식 또는 input 파라미터가 NULL 값인지 아닌지 여부를 확인하기 위한 올바른 구문은 다음과 같다.
singled-valued 경로 표현식 또는 input 파라미터가 NULL 값인지 아닌지 여부를 확인하기 위한 올바른 구문은 다음과 같다.
{single_valued_path_expression | input_parameter}
IS [NOT] NULL
예를 들어, 다음 쿼리는
String ejbql = "SELECT c FROM Customer c
WHERE c.name IS NULL";
Query query = em.createQuery(ejbql);
이름이 없는 모든 고객을 반환한다.
관련 상세 정보
JPQL은 일종의 완벽한 구조화 질의어로, 대규모 업데이트 및 삭제 연산, 외부 조인 연산, 보호, 서브쿼리 등을 위한 언어 특성을 포함하고 있다. 이에 관한 자세한 설명은 Java EE Tutorial의 "Chapter 27: The Java Persistence Query Language"를 참조하기 바란다.
http://java.sun.com/javaee/5/docs/tutorial/doc/QueryLanguage.html#wp80587
예제 코드 실행하기
본 팁에 첨부된 예제 코드를 실행하려면 다음 단계를 수행한다.
1. Java EE 5 SDK를 아직 구하지 못했다면 Java EE 다운로드 페이지에서 다운로드 받아 설치한다.
2. 아래의 환경 변수를 설정한다.
- JAVAEE_HOME. Java EE 5 SDK가 설치된 장소를 표시해야 한다.
- ANT_HOME. ant의 설치 장소를 표시해야 한다. Ant는 다운로드한 Java EE 5 SDK 번들에 포함되어 있다. (Windows에서는 lib\ant 서브디렉토리에 위치함.)
- JAVA_HOME. 사용자 시스템에서의 JDK 5.0의 위치를 가리켜야 한다. JDK는 다운로드한 Java EE 5 SDK 번들에 포함되어 있다. (Windows에서는 jdk 서브디렉토리에 위치함.)
PATH 환경변수에 $JAVA_HOME/bin, $ANT_HOME/bin, $JAVAEE_HOME/bin을 추가한다.
3. 예제 패키지를 다운로드하여 압축을 푼다. ttdec2006jpql 아래의 JPQL 디렉토리에는 예제를 위한 소스 파일과 기타 지원 파일이 포함되어 있다.
4. JPQL 디렉토리로 이동하여 build.xml 파일을 적절히 편집한다. 예를 들어, javaee.home의 값을 Java EE 5 SDK가 설치된 곳으로 설정한다.
5. 데이터베이스 서버를 시작한다.
$JAVAEE_HOME/bin/asadmin start-database
다음과 유사한 출력이 표시되어야 한다.
...
start-db:
[exec] Database started in Network Server mode on host
... and port ...
[exec] Starting database in the background. Log
redirected to ...
[exec] Command start-database executed successfully.
6. 예제 애플리케이션을 구축하고 실행한다. JPQL 디렉토리에서 아래 명령어를 입력한다.
ant all
이 때, 본 팁에서 보여준 JPQL 쿼리가 생성하는 출력이 표시되어야 하고, 그 내용은 아래와 유사한 형태이어야 한다.
...
[java] JPQL query for LIKE/ESCAPE with named parameters
SELECT c from Customer c WHERE c.name LIKE :pattern ESCAPE :e
sc returns Customers: [ejbql.models.Customer@1264eab, ejbql.m
odels.Customer@1e184cb]
[java] JPQL query for LIKE/ESCAPE SELECT c FROM Customer
c WHERE c.name LIKE 'SUN\_%' ESCAPE '\' returns Customers: [e
jbql.models.Customer@1264eab, ejbql.models.Customer@1e184cb]
[java] JPQL query for ENUMERATED type SELECT c FROM Cust
omer c WHERE c.status = :status returns Customers: [ejbql.mod
els.Customer@1e184cb]
[java] JPQL query for IN subquery SELECT o FROM Order o
WHERE o.customer.name IN (SELECT c.name FROM Customer c WHERE
c.customerId > 2) returns Orders: [ejbql.models.Order@1a99347
, ejbql.models.Order@221e9e]
...
[java] JPQL query for Native Query SELECT o FROM Order o
WHERE o.customer.name IN ('foo', 'SUN_SALE') returns Orders:
[ejbql.models.Order@1a99347]
...
[java] JPQL query for NULL handling SELECT c FROM Custom
er c WHERE c.name IS NULL returns Customers: []
글쓴이 소개
Jie Lin Leng은 썬 Java Persistence Engineering 그룹 소속으로, 현재 EJB 3.0을 위한 JPQL 개발에 참여하고 있다.