2022. 7. 28. 17:26ㆍLanguage`/JPA
이전에 엔티티를 쿼리하는 다양한 방법을 짧게 소개했었다.
여기서 가장 중요한 사실은 어떤 방법을 사용하든 JPQL에서부터 시작한다는 사실이다
JPQL의 특징은 다음과 같다
- 테이블을 대상으로 쿼리하는게 아닌 엔티티를 대상으로 쿼리하는 객체지향 쿼리 언어이다
- SQL을 추상화한 쿼리 언어이므로 특정 DB에 종속적이지 않다
>> 결국 JPQL도 최종적으로 DB로 쿼리가 날라갈때는 SQL로 변환되어서 날라간다
JPQL 기본 문법
JPQL은 기본적으로 SQL을 추상화한 쿼리 언어이므로 SQL에서 제공해주는 DML(SELECT, INSERT, UPDATE, DELETE)을 EntityManager를 활용해서 사용할 수 있다
SELECT → em.find(~~), em.createQuery(~~), em.createNativeQuery(~~), ...
INSERT → em.persist(~~)
DELETE → em.remove(~~)
UPDATE → ??
사실 JPQL에서는 엔티티에 대한 Update 쿼리를 특별한 방식으로 날리게 된다.
여기서 사용하는 개념이 "영속성 컨텍스트의 Dirty Checking"이다
@Transactional
public void test3(){
Member findMember = em.find(Member.class, 1L);
findMember.setAge(30);
}
update
member
set
age=30,
team_id=1,
username='Avenus'
where
member_id=1
- 이 테스트의 전제는 DB상에 식별자로 "1"을 가지고 있는 Member Instance가 존재한다는 가정하에 진행한 테스트이다
위의 test3()은 DB로부터 "1L"이라는 식별자를 가진 엔티티를 조회한 후 해당 엔티티의 나이를 변경한 메소드이다
그런데 여기서 알아야하는 사실은 저상태로 코드가 끝난다는 점이고, 바뀐 findMember에 대한 어떠한 저장도 하지 않았는데 쿼리로는 update query가 나갔음을 알 수 있다
처음에 봤을 때는 변경을 한 후에 다시 em.persist를 통해서 영속화 해야 하는게 아닌가라는 고민이 있었지만 이러한 고민은 영속성 컨텍스트를 제대로 이해하고 있다면 전혀 신경쓰지 않아도 될 부분이다
Update에 대한 변경감지 과정은 다음과 같다
- 엔티티를 영속화 시키고 그 시점부터 영속성 컨텍스트의 "엔티티 관리"가 시작된다
- 추후 commit으로 인한 flush를 할 때 "엔티티의 스냅샷"과 현재 엔티티간의 차이를 인식하고 그 차이에 대한 update query를 SQL 저장소에 저장해둔다
- SQL 저장소에 등록된 모든 쿼리를 flush할 때 한번에 DB로 날리게 된다
이러한 변경 감지 기능덕분에 update는 따로 저장하지 않아도 알아서 update query가 생성되어서 DB로 날라가게 된다
- 그리고 추후에 JPQL의 벌크연산과 관련된 포스팅에서 DELETE, UPDTAE는 더욱더 자세히 다룰 예정
조회
JPQL에서는 조회를 할 때 굉장히 여러가지 방법으로 조회를 할 수 있다
- em.find(~~)
- em.createQuery(~~)
- em.createNativeQuery(~~)
JPQL에서 조회를 할 때는 다음과 같은 규칙(?)을 지켜야 한다
1. 대소문자 구분
JPQL에서 엔티티와 속성은 "대소문자를 반드시 구분"해야 한다
물론 SELECT, FROM, ORDER BY, ..와 같은 JPQL 예약어들은 대소문자를 구분하지 않는다
대신에 엔티티 이름이나 엔티티 속성은 반드시 대소문자를 구분해야 한다
2. 엔티티 이름
SELECT m FROM Member m
이러한 JPQL에서 사용한 Member와 같은 것들은 클래스명이 아닌 "엔티티 이름"이다
엔티티 이름은 @Entity내부의 name에 설정해주면 되고 엔티티 이름을 따로 지정해주지 않으면 클래스 이름과 동일하게 사용하면 된다
3. 별칭
JPQL은 엔티티에 대한 별칭을 필수로 사용해야 한다. 만약 별칭을 사용하지 않으면 잘못된 문법이라는 오류가 발생하게 된다
List<String> singleResult = em.createQuery(
"SELECT m.username FROM Member m WHERE m.username like '김%' ORDER BY m.age DESC",
String.class
).getResultList();
위의 JPQL처럼 username이든 age든 전부 Member의 별칭인 m을 추가적으로 붙여줘서 실행해줘야 한다
List<String> singleResult = em.createQuery(
"SELECT username FROM Member m WHERE username like '김%' ORDER BY age DESC",
String.class
).getResultList();
그러면 이렇게 별칭을 붙여주지 않은 JPQL은 쿼리 자체가 날라가지 않을까?'
select
member0_.username as col_0_0_
from
member member0_
where
member0_.username like '김%'
order by
member0_.age DESC
>> 결과적으로는 쿼리가 날라간다
왜냐하면 실질적으로 JPA의 구현체인 Hibernate를 사용하고 있고, Hibernate는 JPQL 표준에 더불어서 추가적인 기능까지 덧붙인 HQL(Hibernate Query Language)를 제공한다
따라서 JPA의 구현체로 Hibernate를 사용하게 되면 별칭 없이 쿼리를 날려도 문제없이 정상적으로 날라간다
TypeQuery, Query
JPQL에 대한 반환 타입에 따라 최종 결과 타입은 TypeQuery와 Query로 나눌 수 있다.
TypeQuery와 Query의 차이점은 다음과 같다
TypeQuery : TypeQuery는 JPQL의 return type이 명확해야 한다
Query : Query는 JPQL의 return type이 불분명할 때 사용
TypeQuery
TypedQuery<Member> query = em.createQuery(
"SELECT m FROM Member m",
Member.class
);
"Member"라는 return type이 특정되었기 때문에 위의 createQuery의 타입으로는 TypeQuery가 지정된 것이다
Query
Query query = em.createQuery(
"SELECT m.username, m.age FROM Member m"
);
반면에 이 JPQL은 return되는 값을 보면 [m.username = String, m.age = int]이므로 하나의 타입으로 묶어줄 수 없다
따라서 이러한 경우 createQuery의 타입으로는 Query가 지정되는 것이다
그리고 Query객체는 SELECT절의 조회 대상이 하나면 Object를 반환하고, SELECT절의 조회 대상이 둘 이상이면 Object[]를 반환한다
List result = em.createQuery(
"SELECT m.username FROM Member m"
).getResultList();
for (Object o : result) {
System.out.println("username = " + o);
}
List result = em.createQuery(
"SELECT m.username, m.age FROM Member m"
).getResultList();
for (Object o : result) {
Object[] value = (Object[]) o;
System.out.println("username = " + value[0]);
System.out.println("age = " + value[1]);
}
결과 조회
JPQL에서 조회한 결과에 대해서 받을수있는 메소드는 총 2가지가 있다
1. getResultList()
getResultList()는 결과를 그대로 List 컬렉션에 감싸서 반환한다
만약 결과가 없더라도 "빈 List 컬렉션"을 반환하게 된다
List<Member> result = em.createQuery(
"SELECT m FROM Member m",
Member.class
).getResultList();
2. getSingleResult()
getSingleResult()는 결과가 "반드시 하나"일 때 사용한다
getSingleResult를 사용해서 결과를 얻으려고 하는데 만약 결과가 둘 이상이면 "예외가 발생한다"
물론 결과가 없어도 예외가 발생한다
Member member = em.createQuery(
"SELECT m FROM Member m WHERE m.id = :id",
Member.class
).setParameter("id", 1L)
.getSingleResult();
그러면 위에서 getResultList()로 받았던 2건의 Member데이터를 getSingleResult로 받으면 진짜 예외가 터지는지 한번 살펴보자
Member member = em.createQuery(
"SELECT m FROM Member m",
Member.class
).getSingleResult();
이렇게 결과가 2건 이상인데 getSingleResult로 받으려고 하면 "NonuniqueResultException"이 터지게 된다
Member member = em.createQuery(
"SELECT m FROM Member m WHERE m.id = :id",
Member.class
).setParameter("id", 3L)
.getSingleResult();
결과가 2건 이상일 때 getSingleResult로 받으면 예외가 터진다는 사실은 파악했다.
그러면 getSingleResult로 받으려고하는데 결과가 없으면 어떻게 될까??
결과가 없는데도 불구하고 getSingleResult로 받으려고 하면 "NoResultException"이 터지게 된다
파라미터 바인딩
JDBC에서는 "위치 기준 파라미터 바인딩"만 지원해주지만 JPQL은 여기에 더불어서 "이름 기준 파라미터 바인딩"까지 지원해준다
1. 위치 기준 파라미터 바인딩
List<Member> members = em.createQuery(
"SELECT m FROM Member m where m.id = ?1 and m.username = ?2",
Member.class
).setParameter(1, 1L)
.setParameter(2, "Avenus")
.getResultList();
위치 기준 파라미터 바인딩을 할 때는 JPQL에 "?"뒤에 위치 순서를 지정해줘서 setParameter에서 값을 동적으로 넣어주면 된다
하지만 위치 기준 파라미터 바인딩은 위치에 의존적으로 파라미터를 바인딩하기 때문에 별로 좋지 못한 방식이다.
따라서 위치 기준 파라미터 바인딩보다는 "이름 기준 파라미터 바인딩"을 필수로 사용하자
List<Member> members = em.createQuery(
"SELECT m FROM Member m where m.id = ?2 and m.username = ?1",
Member.class
).setParameter(1, 1L)
.setParameter(2, "Avenus")
.getResultList();
이처럼 누군가가 악의적으로 파라미터 바인딩의 위치를 바꿔버리면 이는 수많은 실무에서의 코드에서 인식하기 굉장히 어렵기 때문에 결과적으로 쿼리 수행 실패라는 장애를 유발한다
그리고 위치 기준 파라미터 바인딩은 정확히 해당 파라미터가 어떠한 부분에 들어가는지 인식하기 힘들다
2. 이름 기준 파라미터 바인딩
이름 기준 파라미터 바인딩은 말그대로 setParameter에서 파라미터를 바인딩할 때 JPQL 상에서의 이름을 기준으로 파라미터 바인딩을 할 수 있다
List<Member> members = em.createQuery(
"SELECT m FROM Member m where m.id = :id and m.username = :name",
Member.class
).setParameter("id", 1L)
.setParameter("name", "Avenus")
.getResultList();
이처럼 이름 기준 파라미터 바인딩을 하려면 ":"뒤에 바인딩할 이름을 적어주면 설정해주면 된다