Develop/JPA

[자바 ORM 표쥰 JPA 프로그래밍] 20일차 - JPQL 활용 (1)

자라선 2021. 9. 9. 18:37

예시로 사용될 엔티티

더보기
@Data
@Entity
public class Member {

    @Id
    @GeneratedValue
    private long id;

    private String username;

    private int age;

    @ManyToOne
    @JoinColumn(name = "members")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Orders> orders = new ArrayList<Orders>();
}

@Data
@Entity
public class Team {

    @Id
    @GeneratedValue
    private long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

@Data
@Entity
public class Orders {

    @Id
    @GeneratedValue
    private long id;

    private int orderAmount;

    @Embedded
    private Address address;

    @ManyToOne
    @JoinColumn(name = "orders")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "product")
    private Product product;
}

@Data
@Entity
public class Product {

    @Id
    @GeneratedValue
    private long id;

    private String name;

    private int price;

    private int stockAmount;

    @OneToMany(mappedBy = "product")
    private List<Orders> orders = new ArrayList<Orders>();
}

 

 

1. Query, TypedQuery

쿼리로 데이터베이스에서 데이터를 SELECT 할때 타입이 명시되었는지에 따라 반환 타입이 달라진다.

 

        // Member.class 라고 명시 했기 때문에 TypedQuery로 반환
        final TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

        // 제네릭으로 바로 조회된다.
        final List<Member> resultList = query.getResultList();
        for (Member member: resultList) {
            System.out.println("member = " + member.getAge());
        }

위 코드에서 보면 em.createQuery의 2번째 파라미터에 Member.class 라고 반환 타입을 명시 했기 때문에

반환 타입을 TypedQuery와 제네릭 Member 로 하여 조회 할 수있다.

 

작성된 쿼리에서 Member 엔티티를 그대로 SELECT 해주기 때문에 가능하다.

물론 엔티티를 조회 했기 때문에 조회된 엔티티는 영속성 컨텍스트로 관리가 된다.

 

        // 각각 자료형이 다르며 엔티티가 아님
        final Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
        final List resultList = query.getResultList();

        // Object[] 타입으로 반환되며, SELECT에 좌측부터 0, 1, 2 순으로 조회해야한다. 
        for (Object obj : resultList) {
            final Object[] obj1 = (Object[]) obj;
            System.out.println("obj1[0] = " + obj1[0]); // Member.username
        }

엔티티를 그대로 조회하는 것이 아닌 컬럼 단위로 조회를 한다면 

 

Query 타입으로 반환되며 Object[] 배열로 사용한다.

위 쿼리에서 username, age 이기 때문에 Object[0] = username, Object[1] = age 가 된다.

 

엔티티로 조회된것이 아니기 때문에 영속성 컨텍스트로 관리가 안된다.

 

2. 파라미터 바인딩

WHERE 조건절을 사용하기 위해서 조건값을 바인딩 해줄수 있다.

        // 조건 값
        String usernameParam = "멤버";

        // 바인딩변수를 :username 으로 지정해준다. : <- 콜론을 사용하여 변수를 지정 
        final TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);

        // 바인딩 변수에 조건 값을 추가한다.
        query.setParameter("username", usernameParam);
        
        // 조회
        final List<Member> resultList = query.getResultList();
        for (Member member: resultList) {
            System.out.println("member = " + member.getAge());
        }

 

소스상에서 SQL 문자열을 작성할때 문자열을 붙혀 조건값을 추가해줄수는 있으나

이러면 SQL 인젝션의 이슈가 발생하기 때문에 무조건 파라미터 바인딩을 통해 조건 값을 추가해주어야한다.

JDBC API의 PreparedStatement 를 사용하는 개념과 비슷

 

3. 프로젝션

SQL의 SELECT 절에서 조회대상을 지정하는 것을 프로젝션 이라고 부른다.

 

- 엔티티 프로젝션

조회할때 엔티티를 그대로 조회 대상으로 하는 것을 엔티티 프로젝션이라고 한다.

이렇게 조회된 엔티티는 영속성 컨텍스트로 관리가 된다.

SELECT m.team FROM Member m	// 엔티티 프로젝션

 

 

- 임베디드 타입 프로젝션

임베디드 타입으로 설정된 '클래스' 는 주 테이블이 되어 조회할 수 없다. (엔티티가 아니니깐..)

 

임베디드 타입을 사용하는 엔티티에서 임베디드 타입의 값을 조회할 수 있는데

이렇게 조회되면 이 역시 영속성 컨텍스트로 관리가 안된다.

SELECT a FROM Address a;	// 임베디드 타입을 주 테이블로 조회 불가능

SELECT o.address FROM Orders o	// Address 임베디드 타입은 영속성 컨텍스트로 관리 안됨

 

- 스칼라 타입 프로젝션

숫자, 문자 ,날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 부른다.

        // 스칼라 타입, username은 String 이기 때문에 제네릭에도 String 으로 지정한다.
        final List<String> query = em.createQuery("SELECT DISTINCT m.username FROM Member m", String.class)
                                    .getResultList();

 

+ 엔티티 또한 여라개를 동시에 조회할 수 있다.

        // Orders 엔티티에 관계되어있는 다른 엔티티들을 한꺼번에 조회
        final List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Orders o").getResultList();

        // Object 타입으로 반환되며 캐스팅 연산을 통해 엔티티로 변환하는게 가능하다.
        for (Object[] row : resultList) {
            Member member = (Member) row[0];
            Product product = (Product) row[1];
            int orderAmount = (Integer) row[2];
        }

 

 

4. NEW 명령어

// 객체로 한번에 반환 받고싶으나, 안되어 따로 만들어줘야하나?
SELECT username, age FROM Member;

엔티티로 바로 호출되지 못할 경우 우리는 하나의 DTO로 취합하여 최종 반환하게 되는데 이러한 작업을

JPA는 한꺼번에 처리를 도와준다.

 

DTO 클래스 - 조회된 필드들을 이 DTO 로 취합하여 반환다. 

DTO를 만들데 주의점은 기본 생성자와 각 필드에 값을 주입할 생성자가 있어야한다.

@Data
public class UserDTO {

    private String username;
    private int age;

    public UserDTO() {
    }

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

 

        // UserDTO 를 new 와 패키지 경로로 필드를 한번에 묶는다.
        final TypedQuery<UserDTO> query = em.createQuery("SELECT new jpa.entity.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
        
        // 제네릭 타입으로 조회
        final List<UserDTO> resultList = query.getResultList();

쿼리에서 new 패키지경로.DTO클래스(필드1, 필드2) 라고 하여 지정하였다. 

이러면 최종적으로 타입이 지정되었기 때문에 TypedQuery로 반환되며 제네릭으로 바로 사용이 가능하다.

 

5. 페이징 API

각 데이터베이스마다 페이징 처리하는 함수가 다다르기 때문에 JPA는 이를 한번에 지원해주고있다.

        // 페이징
        final TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
        
        query.setFirstResult(10);   // 10 ROW 부터
        query.setMaxResults(20);    // 20 ROW 를 찾음
        query.getResultList();      // 조회

query.setFirstResult 메소드로 조회 시작 위치를 지정하고

query.setMaxResults 메소드로 조회할 데이터의 갯수를 지정한다.

 

 	   select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.members as members4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_ 
        order by
            member0_.username DESC limit ? offset ?

최종 JPA가 생성한 쿼리를  보면 H2의 경우 limit 와 offset 를 통해 페이징 처리를 한것을 볼 수 있다.

 

 

6. 조인

JPA는 조인을 ANSI 규격에 맞춰서 쿼리를 생성해준다.

        // 내부 조인
        String query = "SELECT m FROM Member m INNER JOIN m.team t " +
                        "WHERE t.name = :teamName";

        // 외부 조인
        String query = "SELECT m FROM Member m LEFT JOIN m.team t ON t.name = '팀A' " +
                        "WHERE t.name = :teamName";

        // 컬렉션 조인
        String query = "SELECT m FROM Team t LEFT JOIN t.members m " +
                        "WHERE t.name = :teamName";

		// 메소드 체이닝이 있어 한번에 작성가능
        final List<Member> members = em.createQuery(query, Member.class)
                                    .setParameter("teamName", "팀A")
                                    .getResultList();
        System.out.println(members.size());

일반적인 SQL과 다른 점이라면 JOIN 뒤의 ON을 사용하여 각 테이블끼리 매핑시켜주는 문구가 없는데.

JPA는 서로 관계되어있는 엔티티의 필드끼리 자동 매핑을 하여 쿼리를 생성해준다.

 

 

외부 조인의 경우 OUTER 를 생략하여도 되고 

내부 조인은 INNER 를 생략해도 가능하다.

컬렉션 조인은 @OneToMany의 mappedBy가 되어있는 리스트를 기준으로 테이블을 조회하는 것을 말한다.

 

 

7. 패치 조인

패치조인은 외부 조인과 비슷하지만 모든 엔티티를 조회한다는 차이점이 있다.

        // 패치 조인, FETCH라고 명시한다.
        String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
        final List<Member> members = em.createQuery(jpql, Member.class).getResultList();

		// 이미 Team 또한 영속성 상태가 되었다.
        for (Member member : members) {
            System.out.println("member = " + member.getTeam().getName());
        }

 

사용시 JOIN 뒤에 FETCH 라고 한다.

 

페치조인으로 조회된 엔티티들은 모두 출력된다. 

위 소스의 MemberTeam 엔티티의 모든 컬럼이 조회되어 영속성 상태가 된다.

 

그렇기 때문에 Member.getTeam().getName() 를 호출 하여도 SQL를 생성하여 지연로딩이 발생하지 않고

영속성 컨텍스트에 있는 값을 가져와 출력해준다.

 

이러한 페치 조인을 사용하는 이유는 성능 최적화를 위해서이다.

즉시로딩 방식과 동일한데 즉시로딩을 엔티티에 설정해버리는 것을 '글로벌 로딩 전략' 이러고 부른다.

모든 코드에서 즉시 로딩이 발생하기 때문에 성능 최적화에 있어 힘들게 되어버리는데

 

페치 조인을 사용하여 특정 상황에서만 즉시로딩을 하게하여 성능 최적화를 노리는 것이 좋다.

 

페치 조인 주의점

 - 페치 조인 대상에는 별칭 부여가 안됨

 - 둘 이상의 컬렉션을 페치 하게 된다면 경고가 나온다.

 - 컬렉션 페치 조인을 하면 페이징 API를 사용할 수 없다.