Study/JPA
[JPQL] 기본 문법과 쿼리 API
lsh2613
2023. 9. 14. 19:36
SELECT
- 대소문자 구분
- 엔티티 이름으로 조회 (클래스 이름X)
- 별칭 필수
SELECT를 통해 튜플을 꺼낼 때 Query와 TypedQuery를 사용한다
Query
반환 타입을 명확하게 지정 X
Query query = em.createQuery("select m.username from Member m");
TypedQuery
반환 타입을 명확하게 지정 O
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
결과 조회
getResultList()
- 결과를 예제로 반환
- 결과가 없으면 빈 컬렉션 반환
getSingleResult()
- 결과가 정확히 하나일 때 사용
- 결과가 없거나 하나 초과 시 예외 발생
파라미터 바인딩
이름 기준 파라미터
파라미터를 이름으로 구분
TypedQuery<Member> query
= em.createQuery("select m from Member m where m.username = : username", Member.class);
String usernameParam = "티스토리";
query.setParameter("username", usernameParam);
위치 기준 파라미터
? 다음에 위치 값을 준다. 값은 1부터 시작
String usernameParam = "티스토리";
TypedQuery<Member> query =
em.createQuery("select m from Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam);
참고💡
파라미터 바인딩을 사용하지 않고 직접 문자를 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다.
또한 성능 이슈도 있는데 파라미터 바인딩 방식을 사용하면 파라미터 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용 할 수 있다.데이터베이스 내부에서도 같은 쿼리는 파싱한 결과를 재사용한다.
따라서 전체 성능이 향상되는 효과가 있다.
프로젝션
- 조회할 대상을 지정하는 것
- 프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입
- 조회한 엔티티는 영속성 컨텍스트에서 관리
임베디드 타입
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 임베디드 타입은 영속성 컨텍스트에서 관리하지 않는다.
임베데드 타입을 조회하기 위해선 임베디드와 매핑 되어 있는 엔티티를 조회해야 한다. 즉, 임베디드는 조회의 시작점이 될 수 없다.
@Entity
public class Orders {
...
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "ORDER_ID"))
@Column(name="CITY")
private List<Address> addressHistory = new ArrayList<Address>();
...
}
@Embeddable
public class Address {
String city;
...
}
이와 같이 Orders 엔티티에 Address라는 임베디드 타입이 매핑되어있다.
em.createQuery("select a from address a"); // 오류
em.createQuery("select o.addressHistory from Orders o"); // OK
여러 값 조회
TypedQuery를 사용할 수 없고 대신에 Query를 사용
List<Object[]> resultList =
em.createQuery("select m.age, m.username from Member m").getResultList();
for (Object[] row : resultList) {
UserDTO userDTO = new UserDTO((String) row[0], (String) row[1]);
}
new 명령어
위 코드처럼 직접 객체 변환 작업을 생략할 수 있다
단, 패키지 명을 포함한 전체 클래스 명을 입력해야 하고 순서와 타입이 일치한 생성자가 필요하다.
Query query = em.createQuery("select new hellojpa.UserDTO(m.age, m.username) from Member m");
List<UserDTO> resultList = query.getResultList();
JOIN
- 서로 다른 두 타입을 사용하기 때문에 TypeQuery를 사용할 수 없다.
- INNER JOIN
- 내부 조인, INNER는 생략 가능
- OUTER JOIN
- 외부 조인, OUTER 생략 가능
연관 필드 사용
MEMBER와 TEAM이 연관관계에 있을 때
Query query = em.createQuery("select m.username, t.name from Member m join team t"); // 오류
Query query = em.createQuery("select m.username, t.name from Member m join m.team t"); // OK
세타 조인
- 내부 조인만 지원
- 전혀 관계없는 엔티티도 조인 (=카르테시안 곱)
Query query =
em.createQuery("select m.username, t.name
from Member m, Team t
where m.username = t.name");
페치 조인
- 실제 조인의 종류가 아님
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능
- 별칭 사용 X
엔티티 페치 조인
조인 시 연관딘 엔티티를 같이 조회하는 기능
Query query = em.createQuery("select m from Member m join fetch m.team", Member.class);
List<Member> resultList = query.getResultList();
for (Member member : resultList) {
String username = member.getUsername();
int age = member.getAge();
Team team = member.getTeam();
System.out.println("username = " + username + " age = " + age + " team = " + team.getName());
}
member를 조회할 때 team을 같이 조회하기 때문에 team.name에 접근해도 쿼리가 발생하지 않는다.
컬렉션 페치 조인
조인 시 연관딘 컬렉션을 같이 조회하는 기능
Query query = em.createQuery("select distinct t
from Team t
join fetch t.members
where t.name= '팀A'", Team.class);
List<Team> resultList = query.getResultList();
for (Team team : resultList) {
System.out.println("teamname = " + team.getName());
for (Member member : team.getMembers()) {
System.out.println(" -> member.getUsername() = " + member.getUsername());
}
}
마찬가지로 Team을 조회하는 쿼리만 날렸지만 team에서 member를 조회할 때 새로운 쿼리가 발생하지 않는다.
페치 조인의 특징
- SQL 한 번으로 연관된 엔티티들을 함께 조회 -> SQL 호출 횟수 감소 -> 성능 최적화
- 글로벌 로딩 전략보다 우선
글로벌 로딩 전략
@ManyToOne(fetch = FetchType.EAGER)와 같이 필드레벨에서 선언한 로딩 전략
결론💡
모든 엔티티를 페치 조인을 하면 불필요한 조회가 일어날 수 있다.
따라서 글로벌 로딩 전략은 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
반면 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 여러 테이블에서 필요한 필드만을 조회해 DTO로 반환하는 것이 효과적일 수 있다.
페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다
- 둘 이상의 컬렉션을 페치할 수 없다
- 되기도 하는데 컬렉션*컬렉션의 카테시안 곱이 만들어져서 주의 필요
- 페치 조인 사용 시 페이징 API 사용 불가능
컬렉션 값 연관 경로 탐색 시 주의사항
결론부터 말하면 컬렉션 값에서 경로 탐색을 할 수 없다.
팀을 조회해서 팀의 연관관계인 members를 조회했다고 치자. 이때 member가 갖는 username을 가져오는 경우를 살펴보자.
Query query = em.createQuery("select t.members from Team t"); // OK
Query query = em.createQuery("select t.members.username from Team t"); // 에러
그렇다면 team을 조회하고 team의 연관관계인 member를 조회하려면 어떻게 해야할까?
다음 처럼 새로운 별칭을 획득하면 된다.
Query query = em.createQuery("select m.username from Team t join t.members m");
동적 쿼리
em.createQuery처럼 JPQL을 문자로 완성해서 직접 넘기는 것
정적 쿼리
미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용하는 쿼리, JPQL에선 Named 쿼리 지원
장점
- 로딩 시점에 JPQL 문법을 체크하고 미리 파싱 -> 오류를 빨리 확인
- 사용하는 시점에 파싱된 결과를 재사용 -> 성능상 이점
- 변하지 않는 정적 SQL이 생성 -> DB의 조회 성능 최적화에도 도움
Named 쿼리
말 그대로 쿼리에 이름을 붙여 사용
선언
@Entity
@NamedQuery(
name = "Member.findByUserName",
query = "select m from Member m where m.username=:username"
)
public class Member {
...
}
사용 방법
TypedQuery<Member> namedQuery = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1");