JPA part 10
JPQL
Introduction
JPA에서 재공하는 em.find()를 이용해서 특정 엔티티를 검색할 수 있다. 하지만, 가령 나이 30살 이상인 회원수는 몇명인가? 와 같은 질의를 하기는 어렵다.
그렇다고, 모든 엔티티를 메모리에 올려서 30살 이상인 회원만 필터링하는 것은 비효율적이다.이럴때는 DB가 제공해주는 SQL문을 활용해야하는 데, JPA에서는 JPQL을 이용해서 객체지향적인 쿼리를 수행할 수 있도록 해준다.
JPQL 이외에도 다양한 쿼리 방식을 지원한다.
Query | Description |
---|---|
Criteria Query | JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음 |
Native SQL | JPA에서 JPQL 대신 직접 SQL을 사용 |
QueryDSL | JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음. 비표준 오픈소스 프레임워크 |
JDBC, Mybatis 활용 | JDBC를 이용해서 쿼리를 수행할 수도 있다. |
Member Entity
@Entity(name = "Member")
public class Member {
@Column(name = "name")
private String username;
// ...
}
JPQL 방식
객체 지향형 쿼리를 수행할 수 있고, 특정 DB에 의존하지 않는다.
String jpql="select m from Member as m where m.username='kim'";
List<Member> resultList=em.createQuery(jpql,Member.class).getResultList();
위의 jpql이 아래의 SQL문으로 변환되어 실행된다.
SELECT *
FROM MEMBER M
WHERE M.USERNAME="kim"
Criteria Query
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();
jpql과 달리 메소드 방식으로 수행하기 때문에, 코드 자동 완성 기능이나, 컴파일 단계에서 오류를 발견할 수 있다는 장점이 있다. 다만, 사용하기 불편하다는 단점이 있다.
QueryDSL
Criteria Query 처럼 JPQL을 빌더 역할을 수행한다.
JPAQuery query=new JPAQuery(em);
Qmember member=Qmember.member;
List<Member> members=query.from(member).where(member.username.eq("kim")).list(member);
Qmember는 Query DSL 에서 사용하는 쿼리 전용 클래스로 QueryDSL 사용시 자동으로 만들어진다.
Criteria Query 보다는 훨씬 간결하면서 이해하기 쉽다.
Native SQL
직접적으로 SQL문을 수행하도록 할 수 있다.
String sql = "SELECT ID, AGE, ITEM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList =
em.createNativeQuery(sql, Member.class).getResultList();
native sql는 주로 CONNECT BY, SQL Hint 처럼 특정 DB에서만 동작하는 쿼리들을 수행해야할 때 사용할 수 있다.
JDBC 직접 사용, MyBatis 사용
직접 JDBC Connection에 수행하고자 한다면, JPA 구현체가 제공해주는 방법을 사용해야 한다.(JPA에서는 JDBC Connection을 제공하는 API가 없음)
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLExcetion {
// work ...
}
});
위는 하이버네이트에서 JDBC Connection을 얻는 방법에 대해 다루고 있다.
JDBC 직접 사용, MyBatis 와 같은 SQL 매퍼를 사용하게 되면 기본적으로 JPA를 우회해서 사용하게 되는데, 이렇게 되면 JPA의 영속성 컨텍스트와 DB 간에 데이터 불일치 문제가 발생할 수 있는데, 이를 해결하기 위해 SQL 실행 전에 영속성 컨텍스트에 대해 플러시를 먼저 수행한다.
JPQL Syntax
위의 ERD를 이용해서 JPQL의 기본 문법에 대해 다뤄보자
TypedQuery, Query
jpql 실행을 위해서는 쿼리 객체를 객체를 만들어야한다. 반환되는 타입이 명확한 경우 TypedQuery를, 그렇지 않은 경우 Query 객체를 이용한다.
TypedQuery
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m", Member.class)
List<Member> resultList = query.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
TypedQuery가 제너릭 클래스로 되어 있어, 타입을 명시할 수 있다.
Query
String jpql1 = "select m.username, m.age from Member m;"
Query result = em.createQuery(jpql1);
List resultList = result.getResultList();
for (Object object : resultList) {
Object[] objects = (Object[]) object;
System.out.println("1 = " + objects[0]);
System.out.println("2 = " + objects[1]);
}
여러 컬럼이나, 여러 엔티티에 대해 쿼리를 수행할때는 Query 객체를 사용해야한다. Query를 사용하면 Type Casting을 통해 값을 접근해야한다.
Query Results
query.getResultList(); //결과를 리스트로 반환한다.
query.getSingleResult();//쿼리 결과가 정확히 하나일때만 사용가능하며 그렇지 않은 경우 에러가 발생한다.
query.getSingleResult()를 실행했는데 만일 쿼리 결과가 없으면 NoResultException,결과가 복수개면 NonUniqueResultException 에러 발생한다.
Parameter Binding
이름 기준 바인딩
파라미터를 이름으로 구분 할 수 있다.
String usernameParam = "User1";
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m where m.username = :username",
Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
이름 기준 파라미터 사용시 파라미터 앞에 ‘:’를 붙여준다. 파리미터 이름을 username으로 지정하고, 파라미터 지정시 파라미터 이름을 이용해서 설정할 수 있다.
아래와 같이 체이닝 방식으로 파라미터 값을 바로 지정해줄 수도 있다.
String usernameParam = "User1";
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = :username",
Member.class)
.setParameter("username", usernameParam)
.getResultList();
위치 기준 파라미터 .
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class) .setParameter(1, usernameParam) .getResultList();
위치 기준 파라미터를 사용할때는 ‘:’ 대신에 ‘?’을 사용하며, 인덱스는 1부터 시작한다.
위와 같이 파리미터 방식을 사용해서 쿼리문을 사용해서 SQL Injection같은 공격을 방지할 수 있다.
SELECT
SELECT m From Member AS m where m.username='hello'
- 엔티티와 속성는 대소문자 구분이 된다, SQL 문법(SELECT, FROM 과 같은 예약어)는 대소문자 구분이 없다.
- 테이블 대상이 아니라 엔티티명을 명시한다.
- 별칭을 사용해야 한다.
Projection
SELECT에서 조회할 컬럼을 지정할 수 있다. 프로젝션의 대상이 되는 타입은 엔티티, 엠비디드 타입, 스칼라 타입이다.
Entity Projection
SELECT m FROM MEMBER m
SELECT m.team FROM MEMBER m
객체 단위로 조회를 하는 것이 가능하다. 해당 jpql의 결과 엔티티 즉 객체가 반환된다.
Embedded Type Projection
Entity projection과 동작방식이 유사하지만, 단독으로 Embedded Type에 대한 쿼리를 수행할 수 없다.
아래와 같은 코드는 잘못된 예시이다.
String query = "SELECT a FROM Address a";
위의 쿼리를 올바르게 수행하려면 아래와 같이 엔티티를 통해서 엠비디드 타입에 접근해야한다. 위의 코드가 당연하게 실행안되는 것은 식별자가 없는 데이터 모음 테이블이기 때문이라고 할 수 있다. 이렇게 식별자 없는 값 타입의 경우 영속성 컨텍스트에서 관리하지 않는다.
String query = "SELECT o.address FROM Order o";
List<Address> address = em.createQuery(query, Address.class).getResultList();
Scalar Projection
기본 타입에 대한 조회를 scalar projection이라고 한다.
List<String> usernames =
em.createQuery("SELECT m.username FROM Member m", String.class)
.getResultList();
Multiple Columns
엔티티를 대상으로 조회하는 것이 편하지만, 꼭 필요한 컬럼만 추출해야 되는 경우도 있다. 그럴때는 Query 객체를 활용해야한다.
Query query =
em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
각각의 row 에 대해 Object[] 타입으로 casting을 수행해서 컬럼값을 가져올 수 있다.
Generic을 이용해서 간략화할 수 있다.
List<Object[]> resultList=em.createQuery("SELECT m.username, m.age FROM Member m").getResultList();
for(Object[] row: resultList){
String username = (String) row[0];
Integer age = (Integer) row[1];
}
여러 엔티티를 복합적으로 조회하는 경우에도 Query 객체를 이용해서 한다.
List<Object[]> resultList =
em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0];
Product product = (Product) row[1];
int orderAmount = (Integer) row[2];
}
new 명령어
위의 예제에서 보면 Object[]를 row을 조회하였는데, 해당 row에 대한 클래스를 정의해서 받을 수도 있다.
class UserDTO{
private String username;
private int age;
public UserDTO(String username, int age){
this.username=username;
this.age=age;
}
}
TypeQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
위 처럼, new 명령어를 사용해서 쿼리 수행시 객체 변환작업이 자동적으로 이뤄어 질 수 있도록 할 수 있다. 또 이렇게 타입이 명확해지므로, TypedQuery를 활용할 수 있다. 이때 클래스 이름은 패키지까지 포함한 완전한 형태로 전달해야한다.
Paging
JPA에서는 아래의 API들을 제공한다.
setFirstResult(int startPosition): 조회 시작 위치(rownum)
setMaxResult(int maxResult): 조회할 데이터 갯수
TypedQuery<Member> query= em.createQuery("SELECT m FROM MEMBER m ORDER BY m.username DESC",Member.class);
query.setFirstResult(10);
query.setMaxResult(20);
query.getResultList();
위 처럼 페이징을 지정하면 데이터 11~30번이 조회한다.
JPA에서 위와 같이 페이징을 하도록 메소드를 지정해주면, DB에 맞춰서 적절한 페이징을 구현해준다(DB Dialect 관련)
GROUP BY
tuple에 대해 특정 기준으로 집합을 생성해서 해당 집합에 대한 집합연산을 수행할 수 있다.
// 평균 나이가 10살 이상인 그룹을 조회
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age),
MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
GROUP BY를 이용해서 집합을 묶는 기준을 명시하고, HAVING을 이용해서 조회되는 집합에 대한 조건문을 명시한다.
위의 예제는 회원에 대해서, 팀 이름으로 집합을 묶고, 해당 집합에 각각의 통계 자료를 조회하는 것이다. 단 이때, 팀내 평균 나이는 10이상이 되도록 집합에 조건문을 추가하였다.
ORDER BY
조회되는 컬럼에 대해 정렬을 수행할 수 있다.
select m from Member m order by m.age DESC, m.username ASC
ASC: 오름차순
DESC: 내림차순
JOIN
JPQL에서도 SQL문과 같이 JOIN 기능을 제공한다. JOIN을 통해 연관된 엔티티에 대한 조회를 진행할 수 있다.
Basic Joins
INNER JOIN
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t"
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
위의 jpql는 아래의 sql로 변환된다.
SELECT M.ID, M.AGE, M.TEAM, M.NAME
FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
jpql에서 join을 수행할때는 연관필드를 이용해야 한다는 점이다. 아래의 SQL처럼 join을 구성하면 에러가 발생한다.
SELECT m FROM MEMBER m JOIN TEAM t
OUTER JOIN
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
INNER JOIN과 비슷한 쿼리 문법을 지닌다.
COLLECTION JOIN
일대다, 다대다와 같이 컬렉션을 대상으로한 조인을 수행할 수 있다.
SELECT t,m FROM TEAM t LEFT JOIN t.members m
THETA JOIN
연관관계가 없는 두 엔티티 간에 조인을 수행할 수 있다.
select count(m) from Member m, Team t
where m.username = t.name
위의 jpql은 아래의 sql로 변환된다.
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WHERE
M.USERNAME = T.NAME
JOIN ON
JOIN ON을 이용해서 조인되는 대상을 필터링 할 수 있다.
select m, t from Member m
left join m.team t on t.name = 'A'
SELECT m.*, t.*
FROM Member m
LEFT JOIN Team t ON m.team_id = t.id and t.name = 'A'
위 처럼 외부 조인을 수행하는 경우에 대해 조인 결과에 대한 필터링을 수행할 수 있다.
Advanced Joins
Fetch Join
jpql에서 제공하는 조인 기능으로, 연관된 엔티티, 컬렉션을 한번에 같이 조회해서 성능을 최적화 할 수 있다.
Entity Fetch Join
회원 에티티를 조회하면서 연관된 팀 엔티티를 함께 조회할 수 있다.
String jpql = "select m from Member m join fetch m.team";
위의 jpql을 이용해서 Member을 조회할 때, Team 엔티티도 같이 조회할 수 있도록 하고 있다. Fetch join을 수행할 때는, m.team에 대한 별칭을 사용할 수 없다는 특징이 있다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
//페치조인으로 회원과 팀을 함께 조회 -> 지연로딩 발생 안 함.
System.out.println("username = " + memrber.getUserName() + ", " +
"teamname = " + member.getTeam().name());
...
// 출력결과
/*
username = 회원 1, teamname = 팀A
username = 회원 2, teamname = 팀A
username = 회원 3, teamname = 팀B
*/
}
다음과 같이 멤버 엔티티 조회가 수행되며, 멤버 엔티티를 이용해서 연관된 팀 엔티티에 대한 접근도 가능한 것을 확인할 수 있다.
- 페치 조인을 수행하게 되면 실제 팀 엔티티가 반환되여, 지연 로딩이 수행되지 않는다.
- 프록시 객체를 필요로 하지 않으므로 준영속 상태가 되어도 사용이 가능하다.
Collection Fetch Join
컬렉션에 대한 페치 조인을 수행할 수 있다.
String jpql = "select t from Team t join fetch t.members where t.name="팀A";
위의 sql은 아래로 변환되어 실행된다.
SELECT T.*, M.*
FROM TEAM T INNER JOIN MEMBER M ON T.ID=MEMBER.TEAM_ID
WHERE T.NAME="팀A"
위의 결과를 보면 Team 이름이 “팀A”인 team은 원래 1개만 조회되야하는 게 맞지만, member와 collection fetch join을 수행하면서 2개로 증가되었다.
이렇게 되면 조회를 진행할때, 2개의 team 객체가 반환되는 문제가 일어난다.
String jpql = "select t from Team t join fetch t.members where t.name='팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for (Team team : teams) {
System.out.println("team = " + team.getName() + "| team = " + team);
for(Member member : team.getMembers()){
System.out.println(" ->username ="+member.getUsername()+ " member = " + member);
}
}
이런 문제를 해결하기 위해서 DISTINCT 키워드를 활용할 수 있다.
String jpql = "select DISTINCT t from Team t join fetch t.members where t.name='팀A'";
Collection Fetch vs Inner Join
얼핏 보면 collection fetch 나 inner join 모두 회원, 팀 엔티티에 대한 조회를 하므로 같은 기능을 하는 것 처럼 보인다. 하지만, Inner join 방식에서는 오직 조회한 엔티티에 대한 정보만을 조회할 수 있다. 만약 지연 로딩으로 설정 되어 있다면, Inner Join에서는 연관된 엔티티에 대해 프록시 객체로 접근하게 된다.
따라서, 엔티티에 대한 조인을 수행할때, 연관된 엔티티에 대한 참조가 필요한 경우에는 fetch join을 이용해서 연관된 엔티티까지 모두 가져오는 join 방식을 택한다.
Collection Fetch의 특징
- 별칭을 줄 수 없다.
- 별칭 사용을 통한 잘못된 데이터의 저장을 방지하기 위함
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션과 컬렉션과의 조인을 통해 카테시안 곱이 형성되어 문제가 됨
- 페이징 API를 사용할 수 없다.
- 성능 이슈, 메모리 초과의 문제로 방지하고 있다.
경로 표현식
jpql에서 ‘.’ 을 이용해서 객체를 접근하는 방식으로 경로 표현식이라 한다.
우선 필드 관련 용어 들을 알아보자.
@Entity
public class Member{
@Id @GeneratedValue
private Long id;
@Column(name="name")
private String username;
private Integer age;
@ManyToOne
private Team team;
@OneToMany
private List<Order> orders;
}
|Field Types|Description| |–|–| |상태 필드|username, age 와 같이 엔티티와 관련된 값을 저장하기 위한 필드| |연관 필드|team, orders와 같이 연관관계를 위한 필드나, 임베디드 타입을 저장하기 위한 필드| |단일값 연관 필드|@ManyToOne, @OneToOne과 같이 단일 엔티티에 대응되는 필드| |컬렉션값 연관 필드|@OneToMany, @ManyToMany와 같이 컬렉션에 대응되는 필드|
위에서 보면 각각의 필드에 대한 경로표현식은 아래와 같다.
상태 필드: m.username, m.age
단일값 연관 필드: m.team
컬렉션값 연관 필드: m.orders
연관 필드에 대한 조회를 수행하면 JPA에서는 내부적으로 join을 수행해주는데, 이를 묵시적 조인이라고 한다.
SELECT o.member FROM Order o;
위와 같은 jpql을 실행하면 아래와 같이 내부적으로 join을 수행한다.
SELECT m.*
FROM Order o INNER JOIN Member m on o.member_id=m.id;
그래서 아래와 같은 쿼리를 수행하면 복잡한 조인 쿼리도 내부적으로 자동으로 수행된다.
select o.member.team
from Order o
where o.product.name = 'productA' and o.address.city='JINJU'
위에서는 Order, Member, Team, Product에 대한 조인이 수행된다. address는 임베디드 타입으로 이미 테이블에 포함되어 있어 조인이 수행되지 않는다.
SELECT T.*
FROM Orders o
INNER JOIN Member m ON o.member_id=m.id
INNER JOIN Team t on m.team_id=t.id
INNER JOIN Product p ON o.product_id=p.id
WHERE p.name="productA" AND o.city="JINJU"
위 처럼 이런 복잡한 조인 쿼리를 jpql를 이용한 경로 표현식 형태로 간단하게 수행할 수 있다.
참고
컬렉션값 연관 필드로 대상으로 더 이상의 탐색을 수행할 수 없다. 여러 개의 엔티티와 대응 되기 때문에 컬렉션 대상으로 추가적인 탐색을 수행할 수 없다.
select t.members from Team t //성공
select t.members.username from Team t //실패
추가적인 탐색을 하려면 아래와 같이 별칭을 이용해서 진행해야 한다.
select m.username from Team t join t.members m
컬렉션에 대해 컬렉션 내의 데이터의 개수를 구할 수 있는 size라는 특별한 변수를 제공한다.
select t.members.size from Team t
서브쿼리
sql처럼 서브 쿼리를 수행할 수 있다. JPQL에서는 WHERE 과 HAVING 절에 서브쿼리를 추가 할 수 있다.
나이가 평균보다 많은 회원을 조회하고자 할 때, 서브 쿼리를 이용해서 평균 나이를 구하고 이를 메인 쿼리의 WHERE 절에 포함시킨다.
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
SQL 처럼, EXISTS, ALL, ANY, SOME, IN 과 같은 함수를 사용할 수 있다.
조건식
Data Types
Types | Example |
---|---|
문자 | ‘HELLO’, ‘Hi’ |
숫자 | 10L(Long 타입), 10D(Double 타입), 10F(Float 타입) |
날짜 | {d’2012-03-24’}(DATE 타입), {t’10-11-11’}(TIME), DATETIME {ts’2014-03-24 10-11-11.123’}(DATETIME 타입) |
Boolean | TRUE,FALSE |
Enum | jpabook.MemberType.Admin |
엔티티 타입 | TYPE(m) = Member, 다형성 쿼리를 사용할 때 사용됨 |
Between, In, Like, Null 와 같은 논리 연산식을 사용할 수 있다.
Collection 관련 연산식
IS [NOT] EMPTY
String jpql="select m from Member m where m.orders is not empty"
위의 is not empty 구문은 아래의 서브쿼리 형태로 변환되어 실행된다.
SELECT m.*
FROM Member m
WHERE EXISTS (
SELECT O.id
FROM Order o
WHERE m.id=o.member_id
)
[NOT] MEMBER OF
엔티티가 컬렉션에 포함되어 있는 지 여부를 검사할 수 있다.
SELECT t
FROM Team t
where :memberParam member of t.members
member가 속해 있는 team을 조회할 때 위의 jpql을 활용할 수 있다.
컬렉션에 대해서는 위의 컬렉션 식이 아닌 것에 대해서는 사용할 수 없다.
SELECT m
FROM Member m
WHERE m.orders is null
컬렉션에 is null과 같은 연산식을 사용할 수 없다.
스칼라 식
수학식
+,-,*,/ 과 같은 수학 연산식을 사용할 수 있다.
문자함수
CONCAT, SUBSTRING,TRIM,LOWER,UPPER,LENGTH,LOCATE와 같은 문자열 함수를 사용할 수 있다.
수학함수
ABS,SQRT, MOD, SIZE, INDEX와 같은 수학 함수를 사용할 수 있다.
날짜함수
Functions | Description |
---|---|
CURRENT_DATE | 현재 날짜 |
CURRENT_TIME | 현재 시간 |
CURRENT_TIMESTAMP | 현재 날짜 시간 |
year | 년 반환 |
month | 월 반환 |
day | 일 반환 |
hour | 시간 반환 |
minute | 분 반환 |
second | 초 반환 |
Case 식
기본 CASE
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
Member 나이에 따라 다르게 출력하도록 CASE,WHEN,ELSE 문을 이용해서 표현하는 것이 가능하다.
심플 CASE
select
case t.name
when '팀A' then '인센티브 110%'
when '팀B' then '인센티브 120%'
else '인센티브 105%'
end
from Team t
자바의 switch case문와 유사하며, 기본 CASE와 달리 조건 대상이 있는 경우에는 연산식을 포함할 수 없다.
COALESCE
select coalesce(m.username,m.age,'회원 정보가 없다') from Member m
m.username 부터 순서대로 조사하면서 null 이면 m.age를 조사하고, m.age도 null이면 최종적으로 ‘회원 정보가 없다’가 반환된다. 이처럼 주어진 스칼라 연산식이 null이 아닐 때까지 스칼라 연산식을 순회하면서 null이 아닌 스칼라 연산식을 반환한다.
NULLIF
select NULLIF(m.username, '관리자') from Member m
두개의 스칼라식이 같으면 null을 반환하고, 그렇지 않으면 첫번째 스칼라식을 반환한다.
위의 경우 m.username이 ‘관리자’이면 null을 반환하고, 그렇지 않으면 m.username을 반환한다.
다형성 쿼리
부모 엔티티를 대상으로 조회를 진행하면 자식 엔티티도 함께 조회된다.
Entities
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DisCriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
}
List resultList=em.createQuery("select i from Item").getResultList();
위 처럼 부모 엔티티인 item에 대해 조회를 진행하면 아래와 같이 자식 엔티티도 모두 같이 조회되는 것을 확인할 수 있다.
SELECT
i.ITEM_ID, i.DTYPE,i.name,i.price,i.stockQuantity,
a.artist,a.etc,
m.actor,m.director,
b.author,b.isbn
FROM ITEM i
LEFT OUTER JOIN Album a on i.ITEM_ID=a.ITEM_ID
LEFT OUTER JOIN Movie m on i.ITEM_ID=m.ITEM_ID
LEFT OUTER JOIN Book b on i.ITEM_ID=b.ITEM_ID
Type
타입 함수를 이용해서 자식 타입을 한정지을 수 있다.
SELECT i from Item i
where type(i) in (Book, Movie)
type을 이용해서 자식 엔티티 타입을 book, movie로 한정하고 있다.
SELECT * FROM Item i
WHERE i.DTYPE in ('B','M')
Treat
Treat을 이용해서 부모 엔티티를 자식엔티티로 형변환할 수 있다.
SELECT i from Item i where treat(i as Book).author="kim"
위를 보면 부모 엔티티인 Item을 Book으로 type casting해서 자식 엔티티의 필드에 접근한 것을 볼 수 있다.
SELECT *
FROM ITEM i
WHERE i.DTYPE="B" AND i.author="Kim"
Entity Comparison
jpql에서 엔티티에 대한 비교를 수행하게 되면 자동으로 식별자 끼리 비교를 수행하도록 변환된다.
Primary Key
SELECT COUNT(m.id) FROM Member m
SELECT COUNT(m) FROM MEMBER m
위와 같이 Member 객체를 직접적으로 사용 시 아래와 같이 해당 엔티티에 대한 식별자값으로 대치된다.
SELECT COUNT(m.id)
FROM MEMBER m
Foreign Key
Team team=em.find(Team.class,1L);
String qlString="SELECT m FROM Member m where m.team = :team";
List resultList=em.createQuery(qlString).setParameter("team",team).getResultList();
위의 jpql에서는 연관필드를 통해 객체에 대한 직접 참조를 수행했는데, 위의 jpql는 아래의 sql로 치환된다.
SELECT *
FROM MEMBER M
WHERE M.TEAM_ID=1L
Named 쿼리
Query Types | Description |
---|---|
동적 쿼리 | em.createQuery()와 같이 jpql을 이용해서 직접 넘기는 형태 |
정적 쿼리 | 미리 정의한 쿼리에 이름 부여 후 사용 |
위의 정적쿼리를 NamedQuery라고 한다. 애플리케이션 로딩 시점에 미리 파싱해둬서, 오류를 빨리 발견할 수 있고, 이미 파싱된 결과를 재사용하므로 성능도 뛰어나다
Annotation Style
@NamedQuery
@Entity
@NamedQuery(
name="Member.findByUserName",
query="SELECT m FROM Member m where m.username = :username"
)
public class Member{
}
List<Member> resultList = em.createNamedQuery("Member.findByUsername",Member.class)
.setParameter("username", "회원1")
.getResultList();
위처럼 @NamedQuery로 미리 정의해둔 쿼리를 이용해서 Member 객체를 조회하는 것을 확인할 수 있다.
@NamedQueries을 이용해서 2개 이상의 Named 쿼리를 저장할 수 있다.
XML Style
XML에 NamedQuery 정의
XML 파일에 미리 NamedQuery를 지정해서, 사용하는 방법도 있다.
META-INF/ormMember.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
<named-query name="Member.findByUsername">
<query><![CDATA[
select m
from Member m
where m.name = :username
]]>
</query>
</named-query>
</entity-mappings>
META-INF/persistence.xml
persistence.xml 파일에 아래의 부분을 추가하여 ormMember.xml이 인식될 수 있도록 한다.
<persistence-unit name="jpabook">
<mapping-file>META-INF/ormMember.xml</mapping-file>
NamedQuery 이름에 중복이 있는 경우, 우선 XML에 있는 NamedQuery를 우선적으로 적용한다.
References
book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저
댓글남기기