JPA에서의 JPQL과 SQL 사용 이해
Java Persistence API(JPA)를 사용하면서 JPQL(Java Persistence Query Language)과 SQL을 어떻게 활용하는지 이해하는 것은 중요하다.
JPQL과 SQL은 각각 다른 접근 방식과 목적을 가지고 있다.
JPQL의 기본 구조와 사용 JPQL은 엔티티 객체를 대상으로 쿼리를 작성한다.
이는 SQL과 다르게 데이터베이스 테이블이 아닌, 엔티티 클래스에 직접 쿼리를 수행한다.
아래 쿼리는 Member 엔티티에 대해 이름에 'kim'이 포함된 모든 회원을 조회한다.
기본 jpql
List<Member> memberList =
em.createQuery("select m From Member m where m.username like '%kim%'", Member.class).getResultList();
SQL과의 비교
JPQL 쿼리는 결국 데이터베이스에 SQL로 변환되어 실행된다. 아래는 JPQL 쿼리와 실제 실행되는 SQL이다.
실제 sql
select
From Member m
where m.username like '%kim%'
select
member0_.MEMBER_ID as MEMBER_I1_4_,
member0_.city as city2_4_,
...,
member0_.USERNAME as USERNAME5_4_
from Member member0_
where member0_.USERNAME like '%kim%'
Native SQL 쿼리 사용
createNativeQuery 메소드를 사용하여 JPA에서도 순수 SQL 쿼리를 실행할 수 있다.
List resultList = em.createNativeQuery(
"select MEMBER_ID, city, street, zipcode, USERNAME from Member")
.getResultList();
실제 sql
/* dynamic native SQL query */
select
MEMBER_ID
city,
street,
zipcode,
USERNAME
from
Member
예전에 영속성 컨텍스트를 DB에 반영하는 법은 3가지임을 학습했다.
- em.flush(): 즉시 영속성 컨텍스트를 데이터베이스에 반영한다.
- 트랜잭션 커밋(tx.commit()) 시점에 자동으로 flush가 발생한다.
- JPQL 쿼리 실행 시 자동으로 flush가 발생한다.
만약 JPA와 아무 관련이 없는 SQL로 DB에 값을 넣으려면 flush를 해줘야한다.
// dbconnect.excuteQuery("select * from member')
em.flush()
------------------------------------------------------------------------------------------------------------------------
TypedQuery VS Query
TypedQuery는 반환 타입이 명확할 때 사용된.
반환 타입을 제네릭을 통해 지정할 수 있으며, 이를 통해 타입 안전성을 보장받을 수 있다.
Typedquery 예시
TypedQuery<Member> selectMFromMemberM = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> stringQuery = em.createQuery("select m.username from Member m", String.class);
하지만 반환 타입이 여러개일 경우, TypedQuery가 아닌 Query로 반환타입이 나온다.
Query objectQuery = em.createQuery("select m.username, m.age from Member m", String.class);
getResultList() VS getSingleResult()
getSingleResult()
getSingleResult는 한개를 반환한다.
여기서, 값이 없다면 NoResultException를 반환하고,
결과가 2개 이상이라면 NonUniqueResultException를 반환한다.
Member singleMember = query.getSingleResult();
System.out.println("singleMember.getUsername() = " + singleMember.getUsername());
getResultList()
getResultList는 값이 없다면 빈 리스트를 반환한다.
JPQL 파라미터 바인딩
파라미터를 바인딩하려면 아래와 같이 할 수 있다.
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
Member singleResult = query.setParameter("username", "newnew")
.getSingleResult();
넣을 변수 명을 :를 통해 알려주고, 파라미터에 ("변수명", "넣을값")로 사용하면 된다.
여기서 :username은 파라미터의 이름을 나타내고, setParameter 메소드를 통해 해당 파라미터에 값을 할당한다.
JPQL 프로젝션(SELECT)
프로젝션은 특정 엔티티의 일부 데이터만을 선택하여 조회하는 것을 말한다.
엔티티 프로젝션은 엔티티의 전체 데이터를 조회한다. 아래 예시는 Member 엔티티의 모든 데이터를 조회하는 경우이다.
아래는 객체를 생성 후 DB에 반영한 다음에, 영속성 컨텍스트를 비운 상황이다.
// 엔티티 생성 및 조회
Member member = new Member();
member.setUsername("newnew");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
List<Member> resultList = em.createQuery("select m from Member m", Member.class)
.getResultList();
여기서 Member를 전부 가져오는데,
Member findMember = resultList.get(0);
findMember.setAge(20);
위와 같은 상황이라면 findMember.getAge()를 했을 때 20과 같다면, 이는 엔티티 프로젝션시 영속성 컨텍스트에서 관리되는 것이다.
실제로 반환해보면 true인 것을 알 수 있다.
System.out.println("findMember.getAge() == 20? " + (findMember.getAge() == 20)); // true
System.out.println("true라면, 엔티티 프로젝션시 영속성 컨텍스트에서 관리되는 것이다."); // 영속성 컨텍스트에서 관리 중
jpql에서 엔티티 조인
member로 부터 시작하여 조인하는 쿼리
// 묵시적 조인
List<Team> resultList3 = em.createQuery("select m.team from Member m", Team.class)
.getResultList();
사실, 위와 같이 한다면 예측이 불가능하다. 조인을 어떻게 하는 것인지 알 수 없다.
// 명시적 조인
List<Team> resultList = em.createQuery("select m.team from Member m join m.team", Team.class)
.getResultList();
이와 같이, 어떻게 조인하는지 명시적으로 알려주어야한다. 이는 나중에 경로표현식에서 보다 더 자세히 다룬다.
jpql 임베디드 타입
jpql에서 임베디드타입으로 꺼내는 것 또한 가능하다.
List<Order> resultList = em.createQuery("select o.address from Order o", Order.class)
.getResultList();
주문 중에 주소를 꺼내려면 이렇게 꺼내올 수 있다. 하지만 order에 의존적이며, 이것이 임베디드 타입의 한계이다.
jpql 스칼라타입
스칼라 타입은 무작위의 값이라고 생각하면 편하다. 일종의 규칙이 없고, SQL과 같이 여러종류를 같이 같고 오고 싶을 때 이다.
예를 들어 username과 age를 같이 조회할 때
List<MemberDto> resultList = em.createQuery("select new jpql.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
위 예시에서는 Member 엔티티의 username과 age를 조회하여 MemberDto 객체로 반환한다.
이 방법을 사용하면, 필요한 데이터만을 선택적으로 조회하고 가공할 수 있다.
jpql 조인
inner join
내부 조인을 사용하여 조회하는 쿼리는 다음과 같다.
String query = "select m from Member m inner join m.team t where t.name = :teamName";
List<Member> result = em.createQuery(query, Member.class)
.setParameter("teamName", "teamA")
.getResultList();
쿼리 내에서 inner는 생략 가능하다.
outer join
left outer join을 사용할 때 다음과 같이 outer를 생략할 수 있다. 또한, 조인 대상을 필터링할 때 다음과 같이 사용한다.
String query = "select m from Member m left join m.team t on t.name='teamA'";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
그 외의 JPQL 문법
jpql의 서브쿼리
서브쿼리를 사용하여 한 쿼리 내에서 다른 쿼리를 수행할 수 있다. 아래 예시는 모든 회원의 평균 나이를 조회하는 경우이다.
String query = "select (select avg(m1.age) From Member m1) from Member m left join m.team t on t.name='teamA'";
Double singleResult = (Double) em.createQuery(query).getSingleResult();
쿼리속 쿼리를 넣는 서비쿼리 또한 가능하다.
하지만 JPA에서는 당연히 where 절에 사용가능하지만 from 절에는 불가능하다.
select 속에서 한 번 더 조회하는 것은 JPA에서는 불가능하지만 하이버네이트에서는 가능하다!
jpql enum 타입 조회
String query = "select m from Member m where m.memberType = jpql.MemberType.ADMIN";
다음과 같이 enum 타입으로 조회 가능하다.
jpql 상속관계 확인
예를 들어 item을 상속받는 Book이 있다고 할 때, Book이 Item의 자식인지 확인하는 방법이다.
List<Item> resultList = em.createQuery("select i from Item i where type(i) = Book", Item.class)
.getResultList();
기본 CASE
예를 들어 10살 이상인 객체는 '학생요금'으로, 60살 이하인 객체는 '경로요금'으로 나가게 된다.
반환 타입은 List<String>이며 '학생요금' 또는 '경로요금' 또는 '일반요금' 이 담겨져있다.
String query = "select " +
"case when m.age <= 10 then '학생요금'" +
"when m.age >= 60 then '경로요금'" +
"else '일반 요금' " +
"end " +
"from Member m";
List<String> resultList = em.createQuery(query, String.class)
.getResultList();
jpql COALESCE
조회해서 값이 나오면 반환하고 없으면 뒤에 있는 기본 값을 반환한다.
String query = "select coalesce(m.username, '이름 없는 회원') " +
"from Member m ";
List<String> resultList = em.createQuery(query, String.class)
.getResultList();
조회한 값이 존재하면, 즉 m.username이 null이 아니면, 해당 username을 반환한다.
조회한 값이 존재하지 않으면, 즉 m.username이 null이면, '이름 없는 회원'을 반환한다.
jpql nullIf
두 값이 같다면 null을 반환하고, 다르면 첫번째 값을 반환한다.
예를 들어, 관리자 이름을 감출 때 사용한다.
String query = "select nullif(m.username, '관리자') " +
"from Member m ";
List<String> resultList = em.createQuery(query, String.class)
.getResultList();
'관리자' 라는 이름을 가진 사람이면 null을 반환하고, '관리자'를 제외한 사람은 조회된 값을 반환한다.
jpql 기본함수
concat 함수
a와 b를 연결한다.
String concatQuery = "select concat('a', 'b') " + "from Member m "; // ab 연결
substring 함수
2번째 인자 위치에서 3번째 인자만큼의 글자를 반환한다.
String substringQuery = "select substring(m.username, 2, 3) " +
"from Member m "; // 2번째글자에서 3글자 반환
이렇게 사용 후 username이 abcdefg라면, bcd를 반환한다.
SQL에서 substring 함수에선 인덱스가 1부터 시작한다.
locate 함수
String locateQuery = "select locate('de', 'abcdefg') " +
"from Member m "; // 4반환
de가 시작하는 위치를 찾아 해당 위치의 인덱스를 반환한다.
size 함수
String collectionSize = "select size(t.memberList) " +
"From Team t"; // memberList 크기 반환
컬렉션의 사이즈를 반환한다.
'JPA > ORM 표준 JPA' 카테고리의 다른 글
[ORM 표준 JPA] 11. Embadded, 값 타입 (0) | 2023.10.04 |
---|---|
[ORM 표준 JPA] 10. 프록시와 N + 1 문제 FetchType.LAZY (0) | 2023.10.04 |
[ORM 표준 JPA] 9.고급매핑 - 상속관계 매핑, @MappedSuperclass (0) | 2023.09.30 |
[ORM 표준 JPA] 8.다양한 연관관계 매핑 (0) | 2023.09.30 |
[ORM 표준 JPA] 7. 연관관계 매핑2 양방향 연관관계와 연관관계의 주인 (0) | 2023.09.23 |