본문 바로가기
Framework and Tool/JPA

JPA - 객체지향 쿼리 언어 - QueryDSL 프로젝션

by ocwokocw 2021. 8. 21.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 프로젝션 (Tuple)

조회를 하다보면 하나의 엔티티 형태가 아닌 여러 컬럼으로 이루어진 정보를 조회해야 할 때가 있다. 이때에는 JPQL 에서 살펴본것과 같이 Map 과 비슷한 방식의 Tuple 을 사용하면 된다.

 

JPAQueryFactory query = new JPAQueryFactory(em);

QMember m = new QMember("m");

List<Tuple> members = query.select(m.name, m.age)
	.from(m)
	.where(m.age.gt(10))
	.fetch();
	
members.forEach(member -> {
	System.out.println("Member's name: " + member.get(m.name) +
			", age: " + member.get(m.age));
});

 

Member 의 Entity 를 조회하는게 아니라 이름과 나이만 조회하였다. 그 결과를 Tuple 에다가 저장한다. Tuple 에서 이름과 나이를 가져올때에는 get 으로 가져오는데 문자열이 아니라 Q-Type 의 속성을 이용하여 조회한다.

 

왜 편리하게 String 문자열 형태를 제공하지 않을까라고 생각할 수 있지만 문자열을 제공하면 속성조회시 문자열을 사용하지 않기 위해 생성한 Q-Type 을 사용한 의미가 없어진다. 대신 아래와 같이 index 기반의 접근(0 based index) 방법도 제공한다.

 

members.forEach(member -> {
	System.out.println("Member's name: " + member.get(0, String.class) +
			", age: " + member.get(m.age));
});

- 프로젝션 (Bean 생성)

Tuple 은 사용하기 편하긴 하지만 사용자 입장에서 어떤 속성들을 조회할지 인지하기가 힘들다. 이를 방지하기 위해 DTO 와 같은 특정 객체로 변환하고 싶을 수 있다. 이를 위해 QueryDSL 은 Projections 를 제공하는데 setter, field, 생성자 방식으로 값을 설정하는 메소드를 제공한다. 우선 setter 메소드를 이용한 .bean() 방법부터 살펴보자.

 

public class MemberDTO {

	private Long id;
	private int age;
	private String name;
	
	public MemberDTO() {
		super();
	}

	public MemberDTO(Long id, int age, String name) {
		super();
		this.id = id;
		this.age = age;
		this.name = name;
	}
	
.................

JPAQueryFactory query = new JPAQueryFactory(em);
	
	QMember m = new QMember("m");
	
	List<MemberDTO> members = query
		.select(Projections.bean(MemberDTO.class, m.id, m.name.as("userName"), m.age))
		.from(m)
		.where(m.age.gt(10))
		.fetch();
		
	members.forEach(member -> {
		System.out.println("Member's name: " + member.getUserName() +
				", age: " + member.getAge());
	});

 

위의 코드는 MemberDTO 를 선언하고 Projections 를 사용한 예제이다. select 절에 사용하면 되고 1번째 인자로는 class 형을 지정한다. 2번째 인자부터는 Expression 을 순차적으로 지정하면 된다. Q-Type 의 속성을 지정하면 되는데, m.name 인자를 살펴보면 as 메소드를 이용하여 member 엔티티의 name 속성을 DTO의 userName 에 맵핑하였다.

 

Hibernate: 
    /* select
        m.id,
        m.name as userName,
        m.age 
    from
        Member m 
    where
        m.age > ?1 */ select
            member0_.member_id as col_0_0_,
            member0_.name as col_1_0_,
            member0_.age as col_2_0_ 
        from
            member member0_ 
        where
            member0_.age>?
Member's name: Name#2, age: 13
Member's name: Name#3, age: 32
Member's name: Name#4, age: 22

 

예제를 보면 as 로 userName 으로 SQL 이 생성된것을 알 수 있다. 그리고 회원 이름도 잘 맵핑되었다.

 

Projections.bean 메소드는 setter 를 이용하여 값을 설정한다. 만약 MemberDTO의 setter 메소드를 전부 없애고 public 으로 멤버 변수의 접근제어자를 변경하고 실행하면 오류는 나지 않지만 Sysout 한 결과를 보면 값이 없이 출력된다.

 

Hibernate: 
    /* select
        m.id,
        m.name as userName,
        m.age 
    from
        Member m 
    where
        m.age > ?1 */ select
            member0_.member_id as col_0_0_,
            member0_.name as col_1_0_,
            member0_.age as col_2_0_ 
        from
            member member0_ 
        where
            member0_.age>?
Member's name: null, age: 0
Member's name: null, age: 0
Member's name: null, age: 0

 

프로젝션시 setter 메소드가 아닌 field 를 이용하여 채우고 싶다면 Projections.fields 메소드를 사용한다. 필드의 접근제어자를 private 로 설정하고 setter 없이 getter 만 public 으로 선언하고 실행해도 동작한다.

 

public class MemberDTO {

	private Long id;
	private int age;
	private String userName;
	
	public MemberDTO() {
		super();
	}

	public Long getId() {
		return id;
	}

	public int getAge() {
		return age;
	}

	public String getUserName() {
		return userName;
	}

 

생성자를 이용할 수도 있다. Projections.constructor 를 이용하면 생성자를 이요하는데, 순서가 같은 생성자를 선언해주어야 한다. Projections.constructor 메소드를 보면 생성자의 순서에 맞추어 age 와 name 의 순서를 변경해주었다. 생성자 순서와 일치하지 않게 인자를 넘기면 생성자를 찾을 수 없다고 오류가 나는데 심심하면 실행해보길 바란다.

 

public class MemberDTO {

	private Long id;
	private int age;
	private String userName;

	public MemberDTO() {
		super();
	}

	public MemberDTO(Long id, int age, String userName) {
		super();
		this.id = id;
		this.age = age;
		this.userName = userName;
	}
	
.................

List<MemberDTO> members = query
	.select(Projections.constructor(MemberDTO.class, m.id, m.age, m.name.as("userName")))
	.from(m)
	.where(m.age.gt(10))
	.fetch();

댓글