JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다. JPA가 지원하는 검색 방법은 다음과 같다.
JPQL(Java Persistence Query Language)
Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 작성할 수 있다.
QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음
JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용: 필요에 따라 JDBC를 직접 사용할 수 있다.
이 중 가장 기본이 되는 JPQL에 대해 알아보자. JPQL의 특징은 다음과 같다.
테이블이 아닌 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리이다.
SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. 따라서 데이터베이스 방언(dialect)만 변경하면 JPQL을 수정하지 않아도 데이터베이스 변경이 가능하다.
JPA는 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
Select문은 다음과 같이 사용한다.
select m from Member as m where m.username = 'member1'
엔티티(Member
)와 속성(username
)은 대소문자를 구분한다. 반면 select
, from
, as
와 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
JPQL에서 사용한 Member
는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name='xxx')
로 지정할 수 있으며, 지정하지 않으면 클래스 명이 엔티티 명이 된다.
Member as m
부분을 보면 Member
에 m
이라는 별칭(식별 변수: Identification variable)을 주었다. JPQL은 별칭을 필수로 사용해야 한다. 참고로 as는 생략할 수 있따.
TypeQuery
: 반환 타입이 명확할 때 사용한다.
Query
: 반환 타입이 명확하지 않을 때 사용한다.
TypedQuery<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);
}
Query query = em.createQuery("select m.username, m.age from Member m");
List resultList = query.getResultList();
for(Object o : resultList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
위 예제의 경우, 조회 대상이 회원 이름(String
)과 나이(Integer
)이므로 조회 대상 타입이 명확하지 않다. 이처럼 select 절에서 여러 엔티티나 칼럼을 조회할 때는 반환할 타입이 명확하지 않으므로 Query
를 사용해야 한다.
Query 객체는 조회 대상이 둘 이상이면 Object[]
를 반환하고, 조회 대상이 하나이면 Object
를 반환한다. 예를 들어 select m.username from Member m
이면 결과를 Object
로 반환하고, select m.username, m.age from Member m
이면 Object[]
를 반환한다.
query.getResultList()
: 결과가 하나 이상일 때 리스트 형태로 반환한다.
query.getSingleResult()
: 결과가 하나일 때 단일 객체를 반환한다.
만약 결과가 없으면 NoResultException
예외가 발생한다(spring data jpa에서는 null
이나 optional
을 반환한다).
반면 결과가 둘 이상이면 NonUniqueResultException
예외가 발생한다. 즉 getSingleResult()
는 결과가 정확히 하나일 때가 아니면 예외가 발생한다.
JPQL은 위치 기준 파라미터 바인딩과 이름 기준 파라미터 바인딩 방식을 지원한다. 파라미터가 중간에 추가될 것을 고려해 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 안전하고 명확하다.
:
를 사용한다.Member findMember = em.createQuery("select m from Member m where m.username=:username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
?
뒤에 위치 값을 주면 된다. 위치 값은 1부터 시작한다. Member findMember = em.createQuery("select m from Member m where m.username=?1", Member.class)
.setParameter(1, "member1")
.getSingleResult();
SQL Injection 공격을 방어하는 파라미터 바인딩 방식
파라미터 바인딩 방식을 사용하지 않고 다음과 같이 직접 문자를 더해 JPQL을 만들면 SQL Injection 공격을 당할 수 있다.
"select m from Member m where m.username = '" + usernameParam + "'"
따라서 파라미터 바인딩 방식은 선택이 아닌 필수이다.
em.createQuery("select m from Member m where m.username = :username", Member.class) .setParameter("username", usernameParam) .getResultList();
select 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라 한다. 프로젝션 대상으로는 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
select m from Member m // 회원 조회
select m.team from Member m // 팀 조회
회원과 팀이라는 엔티티를 대상으로 조회한다.
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
Address
를 조회의 시작점으로 사용할 수 없다.String query = "select a from Address a";
String query = "select o.address from Order o";
List<Address> addresses = em.createQuery(query, Address.class)
.getResultList();
List<String> usernames = em.createQuery("select m.username from Member m", String.class)
.getResultList();
distinct
를 사용한다. select distinct m.username from Member m
TypeQuery
를 사용할 수 없고 Query
를 사용해야 한다.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];
}
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
명령어를 사용하면 조회한 값들을 이용해서 dto로 손쉽게 변환할 수 있다. TypeQuery<UserDto> query =
em.createQuery("select new jpabook.jpql.UserDto(m.username, m.age) from Member m",
UserDto.class);
List<UserDto> resultList = query.getResultList();
UserDto
와 같은 클래스명은 패키지 명을 포함하여 작성해주어야 하고, 순서와 타입이 일치하는 생성자가 있어야 한다.public class UserDto {
private String username;
private int age;
public UserDto(String username, int age) {
this.username = username;
this.age = age;
}
}
페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이며, 무엇보다 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다. JPA는 페이징을 다음 두 API로 추상화했다. 따라서 데이터베이스 방언에 따라 JPQL이 적절한 SQL문으로 변환된다.
setFirstResult(int startPosition)
: 조회 시작 위치(0부터 시작)
setMaxResults(int maxResult)
: 조회할 데이터 수
TypedQuery<Member> query = em.createQuery("select m from Member order by m.age desc", Member.class);
query.setFirstResult(0);
query.setMaxResults(10);
query.getResultList();
집합 함수 사용 예는 다음과 같다.
select count(m), // 회원수
sum(m.age), // 나이 합
avg(m.age), // 평균 나이
max(m.age), // 최대 나이
min(m.age) // 최소 나이
from Member m
집합 함수 사용시 주의사항은 다음과 같다.
null
값은 무시되므로 통계에 잡히지 않는다.
값이 없는데 sum
, avg
, max
, min
함수를 사용하면 null
값이 된다. 단 count
는 0이 된다.
distinct
를 집합 함수 안에서 사용해서 중복된 값을 제거하고 집합을 구할 수 있다.
ex, select count(distinct m.age) from Member m
distict
를 count
에서 사용할 때 임베디드 타입은 지원하지 않는다.
group by
는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다. 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
은 group by
와 함께 사용하는데, group by
로 그룹화한 통계 데이터를 기준으로 필터링한다. 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 // 평균나이가 10살 이상인 그룹을 조회
order by
는 결과를 정렬할 때 사용한다.
asc
: 오름차순(기본값)
desc
: 내림차순
select m from Member m order by m.age desc, m.username asc
// 나이를 기준으로 내림차순 정렬, 같으면 이름을 기준으로 오름차순 정렬
회원과 팀을 내부 조인해서 'Team A'에 소속된 회원을 조회하는 JPQL은 다음과 같다.
String teamName = "teamA";
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();
SQL의 조인과 다르게 JPQL의 조인은 연관 필드를 사용한다. 위 예제에서는 m.team
이 연관 필드이다. 그리고 이 연관 필드에 t
라는 별칭을 주었다.
JPQL의 외부 조인은 다음과 같이 사용한다.
select m from Member m left join m.team t
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
회원 -> 팀으로 조인하는 것은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
팀 -> 회원으로 조인하는 것은 일대다 조인이면서 컬렉션 값 연관 필드(t.members)를 사용한다.
예를 들어 다음 코드는 팀과 팀이 보유한 회원들을 컬렉션 값 연관 필드로 외부 조인한다.
select t, m from Team t left join t.members m
where 절을 사용해서 세타 조인을 할 수 있다. 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다. 다음 예제는 전혀 관련없는 Member.username과 Team.name을 조인한다.
select count(m) from Member m, Team t
where m.username = t.name
세타 조인은 내부 조인만 지원한다. 반면 다음에 나오는 join on 절을 사용하면 관계없는 엔티티 간에 외부 조인도 할 수 있다.
on 절을 사용하면 조인 대상을 필터링해서 조인할 수 있다. 그리고 세타 조인과 다르게 연관관계 없는 엔티티 간에 외부 조인도 가능하다.
다음 예제는 회원과 팀을 외부 조인하면서 팀 이름이 'teamA'인 팀만 조인한다.
select m, t from Member m left join m.team t on t.name = 'teamA'
다음 예제는 연관관계가 없는 Member.username과 Team.name을 외부 조인한다.
select m, t from Member m left join Team t on m.username = t.name
감사합니다. 이런 정보를 나눠주셔서 좋아요.