11. [JPA] Querydsl
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.class, 2); 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.class, 2); assertThat("010-2222-2222", is(phoneEntity.getNumber())); em.clear(); // 영속성 컨텍스트 초기화 phoneEntity = em.find(DslPhone.class, 2); // 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.class, 2); 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
Querydsl 버전 히스토리
'프로그래밍' 카테고리의 다른 글
1. Spring Cloud를 이용한 MSA 구축하기 - spring cloud (0) | 2017.09.21 |
---|---|
14. [JPA] Lock - 잠금 (0) | 2017.09.20 |
13. [JPA] 트랜잭션 격리 수준 (Transaction Isolation Level) (0) | 2017.09.12 |
12. [JPA] 복합키 (0) | 2017.09.08 |
spring data elasticsearch 사용해 보기 (6) | 2017.08.31 |
10. [JPA] JPQL (0) | 2017.08.28 |
9. [JPA] @Enumerated (0) | 2017.08.23 |
8. [JPA] Attribute Converter (0) | 2017.08.23 |