본문 바로가기
프로그래밍

11. [JPA] Querydsl

by 탁구치는 개발자 2017. 9. 1.

Querydsl은 오픈 소스 프로젝트이고 type-safe한 쿼리를 위한 Domain Specific Language이다.


왜 필요한가?

SQL query는 문자이다.

이는 type-check가 불가능하고 실행해 보기 전까지 작동여부 확인이 어렵다.

만약 SQL이 class처럼 Type이 있고, Java코드로 작성할 수 있다면 좋지 않을까?

SQL을 java로 type-safe하게 개발 할 수 있게 해주는 프레임워크가 Querydsl이다.

QueryDSL은 JPQL(HQL)을 type-safe하게 작성하기 위해서 만들어졌고 다음처럼 동작한다.

1
Querydsl -> JPQL -> SQL
cs


[Querydsl 테스트 버전]

JPA : 2.1
hibernate : 5.0.12
Querydsl : 4.1.4
cs


Querydsl 버전에 따른 차이에 대해서 한 가지 언급하고 넘어가겠다.

4.0.1 버전까지는 list 조회 시 .list() 메서드를 사용했지만

4.0.2 버전부터는 fetch() 메서드로 이름이 변경되었다.

난 이 부분에 혼선이 있어 살짝 고생했다.


[메이븐 pom.xml 설정]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>4.1.4</version>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>4.1.4</version>
    <scope>provided</scope>
</dependency>
 
<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>
cs


위의 설정은 엔티티를 기반으로 prefix 'Q'가 붙는 클래스들을 자동 생성한다.

즉, Member라는 엔티티 클래스가 있다고 하면 Querydsl에서 QMember라는 클래스를 생성한다.

apt-maven-plugin 은 APT와 통합되어져 있기 때문에 엔티티에 존재하는 애노테이션을 기반으로 'Q'가 붙는 새로운 코드와 파일을 만들어 주는 것이다.


APT란?

Annotation Processing Tool 의 약자로 JDK 1.6 부터 도입되었다.

APT은 Annotation 이 있는 기존코드를 바탕으로 새로운 코드와 새로운 파일들을 만들 수 있고, 이들을 이용한 클래스에서 compile 하는 기능도 지원해준다.


APT를 이용해서 새롭게 생성되어진 QDslMember은 다음과 같은 구조로 생성되어진다.

만약 원본이 되는 DslMember 클래스의 특정 필드가 변경이 되거나 추가된다면 mvn package 를 재실행해야 한다.


[엔티티 생성]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Data
@Entity
@Table(name = "DSL_MEMBER")
public class DslMember {
 
    @Id
    @Column(name = "MEMBER_ID")
    private int id;
 
    private String name;
 
    @OneToOne(fetch = FetchType.LAZY, mappedBy = "member", cascade = CascadeType.PERSIST, optional = false)
    private DslHomeAddress homeAddress;
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "member", cascade = CascadeType.PERSIST)
    private List<DslPhone> phoneList = new ArrayList<>();
 
    public void setHomeAddress(DslHomeAddress homeAddress) {
        this.homeAddress = homeAddress;
        homeAddress.setMember(this);
    }
 
    public void addPhone(DslPhone phone) {
        phoneList.add(phone);
        phone.setMember(this);
    }
}
 
 
@Data
@ToString(exclude = "member")
@Entity
@Table(name = "DSL_PHONE")
public class DslPhone {
 
    @Id
    private int id;
 
    private String number;
 
    private String manufacture;
 
    public DslPhone(int id, String number, String manufacture) {
        this.id = id;
        this.number = number;
        this.manufacture = manufacture;
    }
 
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private DslMember member;
}
 
@Data
@ToString(exclude = "member")
@Entity
@Table(name = "DSL_HOME_ADDRESS")
public class DslHomeAddress {
 
    @Id
    private int id;
 
    private String address;
 
    @OneToOne(optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private DslMember member;
}
cs


엔티티까지 생성했다면 maven goal을 실행하자.

mvn package
cs

실행이 완료되면 프로젝트의 target/generated-source/java 폴더 하위에 Q 가 붙은 java 파일들이 생성되어 있을 것이다.

이들을 이용해서 type-safe한 쿼리를 생성할 수 있다는 게 핵심이다.


모든 준비가 되었으니 테스트를 해보자.

- 조건 검색 하기
- 다른 테이블과 조인하기
- 조인하기
- inner 조인하기
- left 조인하기
- update 하기
- delete 하기
- group by 하기
cs


querydsl을 이용해 쿼리를 작성하기 위해서는 QueryDslRepositorySupport 상속받아야 하며

super(DslMember.class); 처럼 도메인 엔티티 클래스를 슈퍼 타입인 QueryDslRepositorySupport 생성자의 아규먼트로 넘겨줘야 한다.


@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueryDSLTest extends QueryDslRepositorySupport {
 
    @PersistenceContext
    private EntityManager em;
 
    public QueryDSLTest() {
        super(DslMember.class);
    }
 
    @Before
    public void before() {
        DslPhone phone = new DslPhone(1"010-1111-1111""LG");
        DslPhone phone1 = new DslPhone(2"010-2222-2222""SAMSUNG");
        DslPhone phone2 = new DslPhone(3"010-2222-2222""SAMSUNG");
        DslPhone phone3 = new DslPhone(4"010-2222-2222""SAMSUNG");
 
        DslHomeAddress homeAddress = new DslHomeAddress();
        homeAddress.setId(1);
        homeAddress.setAddress("경기도 구리시");
 
        DslMember member = new DslMember();
        member.setId(1);
        member.setName("nklee");
        member.setHomeAddress(homeAddress);
 
        member.addPhone(phone);
        member.addPhone(phone1);
        member.addPhone(phone2);
        member.addPhone(phone3);
 
        em.persist(member);
        em.flush();
        em.clear();
    }
 
    @Test
    @Transactional
    public void Where조건() {
        QDslMember member = QDslMember.dslMember;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.where(member.name.eq("nklee"));
        List<DslMember> result = jpqlQuery.fetch();
        assertThat(1, is(result.size()));
    }
 
    @Test
    @Transactional
    public void Where_다른테이블조건() {
        QDslMember member = QDslMember.dslMember;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.where(member.homeAddress.id.eq(1));
        List<DslMember> result = jpqlQuery.fetch();
        assertThat(1, is(result.size()));
    }
 
    @Test
    @Transactional
    public void Where_리스트형_다른테이블조건() {
        QDslMember member = QDslMember.dslMember;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.where(member.phoneList.any().id.eq(1));
        List<DslMember> result = jpqlQuery.fetch();
        assertThat(1, is(result.size()));
    }
   
    @Test
    @Transactional
    public void join() {
        QDslMember member = QDslMember.dslMember;
        QDslPhone phone = QDslPhone.dslPhone;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.join(member.phoneList, phone);
 
        List<DslMember> result = jpqlQuery.fetch();
        assertThat(2, is(result.size()));
    }
   
    @Test
    @Transactional
    public void innerJoin() {
        QDslMember member = QDslMember.dslMember;
        QDslHomeAddress homeAddress = QDslHomeAddress.dslHomeAddress;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.innerJoin(member.homeAddress, homeAddress);
 
        List<DslHomeAddress> result = jpqlQuery.fetch();
        assertThat(1, is(result.size()));
    }
    
    @Test
    @Transactional
    public void leftJoin() {
        QDslMember member = QDslMember.dslMember;
        QDslHomeAddress homeAddress = QDslHomeAddress.dslHomeAddress;
        QDslPhone phone = QDslPhone.dslPhone;
 
        JPQLQuery jpqlQuery = from(member);
        jpqlQuery.innerJoin(member.homeAddress, homeAddress);
        jpqlQuery.leftJoin(member.phoneList, phone);
 
        jpqlQuery.orderBy(member.name.desc());
 
        List<DslMember> result = jpqlQuery.fetch();
        assertThat(2, is(result.size()));
    }
 
    @Test
    @Transactional
    public void 업데이트() {
        DslPhone phoneEntity = em.find(DslPhone.class2);
        assertThat("010-2222-2222", is(phoneEntity.getNumber()));
 
        // 엔티티 매니저 flush 하지 않아도 아래 구문 실행 시 DB 업데이트 한다. (영속성 컨텍스트에 있는 엔티티는 수정되지 않는다.)
        System.out.println("---------------------------------------------------------");
        QDslPhone phone = QDslPhone.dslPhone;
        update(phone).where(phone.id.eq(2))
                .set(phone.number, "010-3333-3333")
                .execute();
        System.out.println("---------------------------------------------------------");
 
        // 영속성 컨텍스트에 이미 phone 엔티티가 초기화되어 있어 변경되지 않은 핸드폰 번호가 출력된다.
        phoneEntity = em.find(DslPhone.class2);
        assertThat("010-2222-2222", is(phoneEntity.getNumber()));
 
        em.clear(); // 영속성 컨텍스트 초기화
        phoneEntity = em.find(DslPhone.class2); // phone 엔티티 초기화 진행 (변경된 폰번호가 출력된다.)
        assertThat("010-3333-3333", is(phoneEntity.getNumber()));
    }
 
    @Test
    @Transactional
    public void 삭제() {
        QDslPhone phone = QDslPhone.dslPhone;
        delete(phone).where(phone.id.eq(2)).execute();
 
        DslPhone phoneEntity = em.find(DslPhone.class2);
        assertNull(phoneEntity);
    }
 
    @Test
    @Transactional
    public void GroupBy() {
        QDslPhone phone = QDslPhone.dslPhone;
 
        JPQLQuery query = from(phone);
        query.groupBy(phone.number, phone.manufacture);
        query.select(phone.number, phone.manufacture);
 
        List<Tuple> list = query.fetch();
        assertThat(2, is(list.size()));
        for (Tuple tuple : list) {
            System.out.println("number : " + tuple.get(phone.number) + ", manufacture : " + tuple.get(phone.manufacture));
        }
    }
 
    @Test
    @Transactional
    public void GroupByToDto() {
        QDslPhone phone = QDslPhone.dslPhone;
 
        JPQLQuery query = from(phone);
        query.groupBy(phone.number, phone.manufacture);
        query.select(Projections.bean(PhoneDto.class, phone.number, phone.manufacture));
 
        List<PhoneDto> list = query.fetch();
        assertThat(2, is(list.size()));
 
        for (PhoneDto phoneDto : list) {
            System.out.println(phoneDto);
        }
    }
}
cs


참고


apt-maven-plugin 설명

https://github.com/querydsl/apt-maven-plugin


APT 관련 내용

http://docs.oracle.com/javase/6/docs/technotes/guides/apt/index.html

https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657


Querydsl 버전 히스토리

http://www.querydsl.com/releases