✏️ [JPQL] 프로젝션(SELECT), 페이징

박상민·2023년 10월 28일
0

JPA

목록 보기
16/24
post-thumbnail

⭐️ 프로젝션

프로젝션은 셀렉트 절에 조회할 대상을 지정하는 것을 말한다.

  • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
  • 엔티티 프로젝션
    • SELECT m FROM Member m
    • SELECT m.team FROM Member m
  • 임베디드 타입 프로젝션
    • SELECT m.address FROM Member m
  • 스칼라 타입 프로젝션
    • SELECT m.username, m.age FROM Member m
      • DISTINCT로 중복 제거

📌 엔티티 프로젝션

아래 예시를 보자. 엔티티 프로젝션으로 Member 엔티티들을 가져온다.
여기서 질문이 있다.
쿼리에서 불러온 엔티티들이 영속성 켄텍스트에서 관리가 될까?

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setUsername("member1");
            member.setAge(10);
            em.persist(member);

            em.flush();
            em.clear();

            List<Member> result = em.createQuery("select m from Member m ", Member.class)
                    .getResultList(); // Entity들 반환, 반환된 엔티티들은 영속성 켄텍스트에서 관리가 될까?

            Member findMember = result.get(0);
            findMember.setAge(20); //Age가 20으로 변경된다면 영속성 켄텍스트에서 관리가 된다는 것.

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

    }
}

쿼리로 Member를 가져온 뒤 setAge(20)으로 Age의 데이터를 변경했다. 이때, DB에서 Age의 값이 변경된다면 영속성 컨텍스트에서 관리된다는 것이다.

업데이트 쿼리

Hibernate: 
    /* update
        jpql.Member */ update
            Member 
        set
            age=?,
            TEAM_ID=?,
            username=? 
        where
            id=?


업데이트 쿼리가 나갔고 DB를 확인해보니 Age의 값이 20으로 변경되었다. 쿼리로 불러온 엔티티가 영속성 컨텍스트에서 관리된다는 뜻이다.

엔티티 프로젝션을 하면 셀렉트 절에 엔티티가 10~20개 나올 수도 있다. 이 모든걸 영속성 컨텍스트에서 다 관리가 된다.

📌 임베디드 타입 프로젝션

임베디드 타입 Address

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

임베디드 타입 프로젝션

em.createQuery("select o.address from Order o", Address.class)
		.getResultList();

결과 쿼리

Order o */ select
    order0_.city as col_0_0_,
    order0_.street as col_0_1_,
    order0_.zipcode as col_0_2_ from
        ORDERS order0_

Order에서 값 타입인 adress를 셀렉트한 결과이다. 결과를 보니 Adress와 관련된 것만 잘 가져오는 것을 볼 수 있다.

임베디드 타입 프로젝션은 한가지 한계가 있는데

em.createQuery("select address from Order o", Address.class)
		.getResultList();

위처럼 address로 바로 받아오지는 못한다는 것이다. Adress는 값 타입으로 어딘가에 소속되어있기 때문에 어디 소속인지 엔티티를 정해줘야한다.

📌 스칼라 타입 프로젝션

em.createQuery("select distinct m.username, m.age from Member m")
        .getResultList();

스칼라 타입 프로젝션은 일반 SQL의 셀렉트 프로젝션과 거의 똑같다.

이때 고민이 하나 생긴다. 응답 타입이 2개인데 반환 타입을 어떻게 가져와야할까?

총 3가지 방법이 있다. 아래에서 하나하나 소개하겠다.

📌 프로젝션 - 여러 값 조회

✔︎ Query 타입으로 조회

List resultList = 
		em.createQuery("select m.username, m.age from Member m")
			.getResultList();

Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("username = " + result[0])
System.out.println("age = " + result[1])

//결과
username = member1
age = 10

✔︎ Object[] 타입으로 조회

List<Object[]> resultList =em.createQuery("select m.username, m.age from Member m")
                .getResultList();
                
Object[] objects = resultList.get(0);
System.out.println("username = " + objects[0]);
System.out.println("age = " + objects[1]);

//결과
username = member1
age = 20

✔︎ new 명령어로 조회

public class MemberDTO {
    private String username;
    private int age;

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
    // Getter, Setter...
}
List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
                    .getResultList();

MemberDTO memberDTO = resultList.get(0);
System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());
System.out.println("memberDTO.getAge() = " + memberDTO.getAge());

//결과
memberDTO.getUsername() = member1
memberDTO.getAge() = 20
  • 단순 값을 DTO로 바로 조회
    • SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
  • 패키지 명을 포함한 전체 클래스 명 입력
  • 순서와 타입이 일치하는 생성자 필요

new 명령어로 조회하는 방법이 가장 깔끔하다. 근데 패키지가 길어진다면 다 적어주는게 너무 길고 문자이기 때문에 한계가 있다.

이런 부분들도 QueryDSL을 사용하면 다 극복이 된다.

⭐️ 페이징

JPA는 복잡한 페이징을 다음 두 API로 추상화했다.

  • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수

페이징 API 예시

String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
	.setFirstResult(10)
	.setMaxResult(20)
	.getResultList();
  • 모든 데이터 베이스의 방언이 동작한다.

위 쿼리의 MySQL 방언

SELECT
  M.ID AS ID,
  M.AGE AS AGE,
  M.TEAM_ID AS TEAM_ID,
  M.NAME AS NAME
FROM
  MEMBER M
ORDER BY
  M.name DESC LIMIT ?, ?

위 쿼리의 Oracle 방언

SELECT * FROM
  ( SELECT ROW_.*, ROWNUM ROWNUM_
   FROM
      ( SELECT
          M.ID AS ID,
          M.AGE AS AGE,
          M.TEAM_ID AS TEAM_ID,
          M.NAME AS NAME
         FROM MEMBER M
         ORDER BY M.NAME
        ) ROW_
    WHERE ROWNUM <= ?
    )
WHERE ROWNUM_ > ?

출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글