Lined Notebook

[Spring Data JPA] 28. JPA 프로젝션과 페이징 그리고 조인

by ymkim

⚡ 01. 프로젝션

RDBMS의 SQL은 기본적으로 스칼라 타입 데이터만 가져온다. 하지만 JPA에서는 아래와 같이 엔티티, 임베디드 타입, 스칼라 타입 중 하나의 타입을 가져올 수 있다.

SELECT m FROM Member -- 엔티티 프로젝션
SELECT m.team FROM Membmer m --엔티티 프로젝션
SELECT m.address FROM Member m -- 임베디드 타입 프로젝션 (값 타입)
SELECT m.username, m.age FROM Member m --  스칼라 타입 프로젝션
  • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상
    • 엔티티(Entity)
    • 임베디드 타입
    • 스칼라 타입(숫자, 문자등 기본 데이터 타입)
  • DISTINCT로 중복 제거

✅ 01-1. 프로젝션 - 여러 값 조회

  1. Query 타입으로 조회
  2. Object [] 타입으로 조회
  3. new 명령어로 조회
    • 단순 값을 DTO로 바로 조회
    SELECT new jpabook.jpql.UserDTO(m.useraname, m.age) FROM Member m
    
    • 패키지 명을 포함한 전체 클래스 명 입력
    • 순서와 타입이 일치하는 생성자 필요

✅ 01-1. Query 타입으로 조회

TypeQuery<JpqlMember> memberList = em.createQuery("select m from JpqlMember m", JpqlMember.class);
  • 가장 기본적인 예제

✅ 01-2. Object 타입으로 조회

// 💡 : m.username, m.age인데 타입은 어떤걸로 지정해야 하지??
// ⚡ : 첫 번째 방법,  Object -> Object [] 다운 캐스팅
List resultList = em.createQuery("select distinct m.username, m.age From JpqlMember m")
        .getResultList();

Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("result = " + result[0]);
System.out.println("result = " + result[1]);
// ⚡ : 두 번째 방법, 제네릭 타입을 Object [] 로 지정
List<Object[]> resultList = em.createQuery("select distinct m.username, m.age From JpqlMember m")
                    .getResultList();

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

위에서 말했다시피 m.username, m.age를 조회하는 상황이기 때문에 Object 타입을 다운 캐스팅(Down Casting)하여 Object [] 형태로 받아야 한다. 후에 값을 조회한 결과는 다음과 같다.

Hibernate:
    /* select
        distinct m.username,
        m.age
    From
        JpqlMember m */ select
            distinct jpqlmember0_.username as col_0_0_,
            jpqlmember0_.age as col_1_0_
        from
            JPQL_MEMBER jpqlmember0_
result = member1
result = 10
  • 스칼라 타입 프로젝션의 데이터 값 조회 결과

✅ 01-3. new 명령어로 조회

가장 깔끔한 방법

DTO 생성

public class JpqlMemberDTO {

    private String username;
    private int age;

    public JpqlMemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
// 05. DTO로 받음, qlString이 문자기 때문에 패키지명을 모두 적어줘야 한다.
//      QueryDSL에서 이러한 문제 극복 가능
List<JpqlMemberDTO> result = em.createQuery("select new com.jpql.JpqlMemberDTO(m.username, m.age) From JpqlMember m", JpqlMemberDTO.class)
        .getResultList();

JpqlMemberDTO memberDTO = result.get(0);
System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
  1. 별도의 DTO 클래스를 생성해준다.
  2. 후에 제네릭 타입을 생성한 DTO 타입으로 설정 해준다.
  3. 위와 같이 간단하게 조회가 가능.

⚡ 02. 페이징 API

ORACLE, MS-SQL을 사용해본 사람은 페이징 쿼리를 작성하는 것이 간단하면서도 얼마나 복잡한지 알 것이다. 하지만 JPA는 다음과 같이 두 개의 API로 페이징 처리를 추상화 해두었다.

✅ 02-1. JPA는 페이징을 다음 두 API로 추상화

시작 위치, 조회 데이터 수

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

✅ 02-2. JPA 페이징 예제

try {
    for (int i = 0; i < 100; i++) {
        JpqlMember member = new JpqlMember();
        member.setUsername("member" + i);
        member.setAge(i);
        em.persist(member);
    }
    em.flush();
    em.clear();

    // 01. 페이징 처리
    List<JpqlMember> resultList = em.createQuery("select m From JpqlMember m order by m.age desc", JpqlMember.class)
//                    .setFirstResult(0) // idx
//                    .setFirstResult(1)
//                    .setMaxResults(10)
                    .setFirstResult(35)
                    .setMaxResults(10) // 최대 갯수 지정 35 ~ 45
                    .getResultList();

    System.out.println("memberList.size = " + resultList.size());
    for (JpqlMember result : resultList) {
        System.out.println("result = " + result);
    }

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
Hibernate:
    /* select
        m
    From
        JpqlMember m
    order by
        m.age desc */ select
            jpqlmember0_.id as id1_10_,
            jpqlmember0_.age as age2_10_,
            jpqlmember0_.TEAM_ID as team_id4_10_,
            jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_
        order by
            jpqlmember0_.age desc limit ? offset ? // db 방언
memberList.size = 10
result = JpqlMember{id=99, username='member98', age=98}
result = JpqlMember{id=98, username='member97', age=97}
result = JpqlMember{id=97, username='member96', age=96}
result = JpqlMember{id=96, username='member95', age=95}
result = JpqlMember{id=95, username='member94', age=94}
result = JpqlMember{id=94, username='member93', age=93}
result = JpqlMember{id=93, username='member92', age=92}
result = JpqlMember{id=92, username='member91', age=91}
result = JpqlMember{id=91, username='member90', age=90}
result = JpqlMember{id=90, username='member89', age=89}
3월 13, 2022 11:38:31 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

Process finished with exit code 0

페이징 파트에서 짚고 넘어가야 하는 부분은 setFirstResult를 사용하여 시작 위치를 지정할 수 있다는 점과, setMaxResults를 사용하여 최대 갯수를 지정할 수 있다는 부분이다.

✅ 02-3. 페이징 API - MySQL 방언

단일, 단중행, 다중컬럼 서브쿼리 인라인 뷰(inline view) 스칼라 서브쿼리

SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM
    MEMBER MEMBER
ORDER BY
    M.NAME DESC LIMIT ?, ?
  • MySQL의 페이징 처리 방언
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_ > ?
  • ORACLE의 페이징 처리 방언
  • 인라인뷰로 3 depth 처리

⚡ 03. 조인

✅ 03-1. 내부 조인(INNER JOIN)

-- 엔티티를 대상으로 조회하기 때문에 m.team과 같이 join을 설정한다.
-- 기본 조인이 INNERT JOIN
SELECT m FROM Member m [INNER] JOIN m.team t
// 1. innert join
String query = "select m from JpqlMember m inner join m.team t";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();
  • innert join 쿼리 작성
Hibernate:
    /* select
        m
    from
        JpqlMember m
    inner join
        m.team t */ select
            jpqlmember0_.id as id1_10_,
            jpqlmember0_.age as age2_10_,
            jpqlmember0_.TEAM_ID as team_id4_10_,
            jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_
        inner join // ⚡ innter join
            JPQL_TEAM jpqlteam1_
                on jpqlmember0_.TEAM_ID=jpqlteam1_.TEAM_ID
Hibernate:
    select // ⚡ : 쿼리 한방 날렸는데, 이 부분은 뭐지? 라는 생각이 들어야함
        jpqlteam0_.TEAM_ID as team_id1_13_0_,
        jpqlteam0_.name as name2_13_0_
    from
        JPQL_TEAM jpqlteam0_
    where
        jpqlteam0_.TEAM_ID=?

아래에서 한 번더 설명하겠지만, @ManyToOne, @OneToOne으로 Team과 Member가 연관관계를 가지고 있기 때문에 쿼리가 2번 날라가는 현상이 발생했다. 즉, 즉시로딩으로 설정이 되어 있기 때문에 중복 호출이 되었다는 의미다.

저번에 공부 하였던 즉시로딩, 지연로딩의 주의사항을 다시 한번 짚고 넘어가자

🔥 03-2. 일대다(1 : N) 주의 사항 복기

@Entity
public class Team {
    //..

    @OneToMany(mappedBy = "team")
    private List<JpqlMember> memberList = new ArrayList<>();

    //..
}
@Entity
public class Member {
    //..

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private JpqlTeam team;

    //..
}

프록시와 즉시 로딩 주의 사항은 해당 링크를 참고해주세요.

@ManyToOne, @OneToOne은 기본(Default) 값이 즉시로딩으로 설정이 되어있다. 그러므로 fetch = FetchType.LAZY 설정이 필요하다, 아니면 쿼리가 여러번 날라갈 수 있다.

✅ 03-3. 외부 조인(LEFT | RIGHT OUTER JOIN)

-- 엔티티를 대상으로 조회하기 때문에 m.team과 같이 join을 설정한다.
-- 왼쪽 테이블 기준으로 모든 데이터 조회
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
// 01. [inner] join
/*String query = "select m from JpqlMember m join m.team t";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();*/

// 02. left [outer] join
String query2 = "select m from JpqlMember m left outer join m.team t";
List<JpqlMember> resultList2 = em.createQuery(query2, JpqlMember.class)
        .getResultList();
  • left outer join 쿼리 작성
Hibernate:
    /* select
        m
    from
        JpqlMember m
    left outer join
        m.team t */ select
            jpqlmember0_.id as id1_10_,
            jpqlmember0_.age as age2_10_,
            jpqlmember0_.TEAM_ID as team_id4_10_,
            jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_
        left outer join // ⚡ left outer join
            JPQL_TEAM jpqlteam1_
                on jpqlmember0_.TEAM_ID=jpqlteam1_.TEAM_ID
3월 13, 2022 12:42:06 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

Process finished with exit code 0

✅ 03-4. 세타 조인

-- 연관관계 없는 상황에서 쿼리 조회?
select count(m) from Member m, Team t where m.username = t.name
// 01. [inner] join
/*String query = "select m from JpqlMember m join m.team t";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();*/

// 02. left [outer] join
/*String query2 = "select m from JpqlMember m left outer join m.team t";
List<JpqlMember> resultList2 = em.createQuery(query2, JpqlMember.class)
        .getResultList();*/

// 03. seta join, cross join
String query = "select m from JpqlMember m, JpqlTeam t where m.username = t.name";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();

System.out.println("resultList = " + resultList.size());

Cross Join 이라 하며, A 테이블을 기준으로 B 테이블의 동일한 데이터를 모두 매핑 시켜 데이터를 출력하는 조인 방식이다.

Hibernate:
    /* select
        m
    from
        JpqlMember m,
        JpqlTeam t
    where
        m.username = t.name */ select
            jpqlmember0_.id as id1_10_,
            jpqlmember0_.age as age2_10_,
            jpqlmember0_.TEAM_ID as team_id4_10_,
            jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_ cross
        join
            JPQL_TEAM jpqlteam1_
        where
            jpqlmember0_.username=jpqlteam1_.name
resultList = 1
3월 13, 2022 12:59:05 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

Process finished with exit code 0

✅ 03-5. 조인 - ON 절

-- DB SQL
SELECT *
FROM test1 a LEFT JOIN test2 b
ON (a.aa = b.aa)
WHERE b.aa IS NULL;
  • ON절을 활용한 조인(JPA 2.1부터 지원)
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

💡 03-5-1. 조인 대상 필터링

ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

jpql

SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'

SQL

-- Member의 id == team의 id && t.name == A인 경우
SELECT m.*, t.*
FROM Member m LEFT JOIN Team t
ON m.TEAM_ID = t.id and t.name = 'A'

JAVA(JPQL)

// 호출
String query = "select m from JpqlMember m left join m.team t on t.name = 'teamA'";
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();
// 출력문
Hibernate:
    /* select
        m
    from
        JpqlMember m
    left join
        m.team t
            on t.name = 'teamA' */ select
                jpqlmember0_.id as id1_10_,
                jpqlmember0_.age as age2_10_,
                jpqlmember0_.TEAM_ID as team_id4_10_,
                jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_
        left outer join
            JPQL_TEAM jpqlteam1_
                on jpqlmember0_.TEAM_ID=jpqlteam1_.TEAM_ID
                and (
                    jpqlteam1_.name='teamA' // ⚡ join 조건문 안에 이 부분이 추가됨
                )
3월 13, 2022 1:21:58 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

Process finished with exit code 0

💡 03-5-2. 연관관계가 없는 엔티티 외부 조인

ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

JPQL

SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name

SQL

-- Member의 id == team의 id && t.name == A인 경우
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t
ON m.username = t.username

JAVA

String query = "select m from JpqlMember m left join Team t on m.username = t.name"; // 막 조인
List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
        .getResultList();
Hibernate:
    /* select
        m
    from
        JpqlMember m
    left join
        Team t
            on m.username = t.name */ select
                jpqlmember0_.id as id1_10_,
                jpqlmember0_.age as age2_10_,
                jpqlmember0_.TEAM_ID as team_id4_10_,
                jpqlmember0_.username as username3_10_
        from
            JPQL_MEMBER jpqlmember0_
        left outer join
            Team team1_
                on (
                    jpqlmember0_.username=team1_.name
                )
3월 13, 2022 1:39:25 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpashop]

Process finished with exit code 0

참고 자료

블로그의 정보

기록하고, 복기하고

ymkim

활동하기