BackEnd/JPA

[JPA] 객체지향 쿼리 언어 4 (중급 문법)

샤아이인 2022. 4. 10.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

4. 다형성 쿼리

● TYPE

- 조회 대상을 특정 자식으로 한정한다.

예를 들면, Item 중 Book, Movie를 조회해라 와 같은 JPQL은 다음과 같다.

//JPQL
select i from Item i where type(i) IN(Book, Movie)

//SQL
select i 
from Item i 
where i.DTYPE in('B', 'M');
 

● TREAT (JPA2.1)

- 자바에서의 다운캐스팅과 유사하다.

- 상속 구조에서 부모타입을 특정 자식 타입으로 다루기 위해 사용한다.

- FROM, WHERE, SELECT 절에 사용가능

예를 들면, Item을 select하고 싶은데, Item의 저자정보를 이용하고 싶다.

하지만 Item에는 저자 필드가 없다. 자식인 Book에 가야 저자 필드가 있다.

//JPQL
select from Item i where treat(i as Book).author = 'kim';

//SQL[
select i.* 
from Item i 
where i.DTYPE = 'B' and i.author = 'kim';
 

5. 엔티티 직접 사용

● JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본키 값을 사용하게 된다.

// JPQL
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용

// SQL(JPQL 둘 다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
 

● 파라미터를 엔티티로 넘겨주거나, 식별자를 넘겨주더라도 실행되는 SQL이 같다.

/*엔티티를 파라미터로 전달*/
String jpql = "select m from Member m where m = :member";
List resultList = em.createQuery(jpql).setParameter("member", member).getResultList();

/*식별자를 직접 전달*/
String jpql = "select m from Member m where m.id = :memberId";
List resultList = em.createQuery(jpql).setParameter("memberId", memberId).getResultList();
 

위에서 두 JPQL 모두 다음과 같은 SQL로 바뀌게 된다.

select m.* from Member m where m.id = ?
 

● 왜래 키 값을 사용할때도 동일하다.

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();

String qlString = “select m from Member m where m.team.id = :teamId”; 
List resultList = em.createQuery(qlString)
                    .setParameter("teamId", teamId) 
                    .getResultList();

// 실행된 SQL
select m.* 
from Member m 
where m.team_id = ?
 

위 코드에서 m.team은 당연히 Member의 FK에 해당된다.

 

6. Named 쿼리

- 미리 정의해서 이름을 부여해 두고 사용하는 JPQL이다.

- 정적쿼리 이다.

- 에노테이션 또는 XML로 정의할수 있다.

@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();

 

- 애플리케이션 로딩 시점에 초기화 후 재사용 한다. => 로딩 시점에 JPQL 쿼리 문법 오류 검증 가능

JPA는 결국 SQL로 parsing 되어 사용되는데 로딩 시점에 한번 초기화가 된다면 parsing cost를 절약 가능하다.

 

나중에 Spring Data JPA 를 사용할때 Named 쿼리가 내부적으로 다 사용된다.

 

7. 벌크 연산

일반적으로 SQL의 update, delete문이라 생각하면 된다.

 

예를 들어 회원의 나이를 전부 20로 변경하려면 어떻게 해야 할까? 다음 예시 코드를 살펴보자.

Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

// flush 자동 호출
int resultCount = em.createQuery("update Member m set m.age = 20") // 이부분이 핵심!!!
        .executeUpdate();

System.out.println("resultCount = " + resultCount);
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getAge());

transaction.commit();
 

실행결과 resultCount 는 3이 나오며, DB는 다음과 같이 변했다.

DB상에서는 나이가 전부 20으로 잘 변경되었다. 하지만 엔티티에서는 나이가 아직 0살이다.

벌크 연산후, find로 찾아온 findMember의 나이를 보면 아직 0살로 되어있다.

Member findMember = em.find(Member.class, member1.getId());
 

이런문제는 어떻게 발생한것 일까? 벌크 연산의 주의 사항을 알아야 한다.

 

● 벌크 연산의 주의사항

 

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날린다.

 

1. 영속성 컨텍스트에 아무것도 없을때,

처음부터 벌크 연산을 먼저 수행, 그럼 영속성 컨텍스트에 아무것도 없으니 괜찮다.

 

2. 영속성 컨텍스트에 뭐가 있을때,

벌크연산 수행 후 영속성 컨텍스트 초기화해야 한다.

벌크 연산도 JPQL이기 때문에 쿼리가 나가서, flush는 자동으로 선행적으로 수행된다.

이후 영속성 컨텍스트만 초기화 해주면 된다.

 

위에서 봤던 코드를 다음과 같이 고쳐보자.

int resultCount = em.createQuery("update Member m set m.age = 20")
        .executeUpdate();
em.clear(); // 초기화가 추가!!!!!!!!!!!!!!!!!!!!!!!

System.out.println("resultCount = " + resultCount);
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getAge());
 

결과는 다음과 같다.

resultCount 출력 후, clear()하였기 때문에 Select 쿼리가 새로 나가고, Member의 나이는 조회해온 20으로 출력된다.

댓글