[Querydsl] 실전 Querydsl

Junho Bae·2021년 5월 30일
2

SPRING JPA

목록 보기
5/5

실전! Querydsl

김영한님의 인프런 실전! Querydsl을 듣고 정리한 내용입니다. 실전! Querydsl 링크

Querydsl 소개

Querydsl?

Java 최신 기술의 마지막 퍼즐.
스프링 부트 + 스프링 Data Jpa로 해결하지 못하는 문제?
1. 복잡한 쿼리
2. 동적인 쿼리
이 문제를 깔끔하게 해결해 주는 Querydsl.
1. 쿼리를 자바 코드로 작성
2. 문법 오류를 컴파일 시점에 알 수 있음.
3. 동적 쿼리 문제 해결!
4. 쉬운 SQL 스타일 문법

demo


@Test
public void jpql() {
	String username = "kim";
	String query = "select m from Member m" + "where m.username =:username";
	//실수 : 실제 쿼리는 "select m from member mwhere m.username=:username"	

	//자바 컴파일러가 이를 잡아주지 못함. 실행해봐야 알 수 있음.

	List<Member> result = em.createQuery(query, Member.class).getResultList();
}

@Test
public void querydsl() {
	String username = "kim";
	List<Member> result = queryFactory
				.select(member)
				.from(member)
				.where(member.username.eq(username))
				.fetch();
	//자동완성 가능
	//메서드로 extract가능
	//띄어쓰기, 오타 걱정 없음

}

프로젝트 환경 설정

프로젝트 생성, Querydsl 설정과 검증

프로젝트 생성은 다른 프로젝트 생성과 동일.
1. Querydsl 설정


plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    //querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

dependencies {
	...
    //querydsl
    implementation 'com.querydsl:querydsl-jpa'
	...
}

//querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
//에디터 설정
sourceSets {
    main.java.srcDir querydslDir
}
//컴파일 클래스 패스에 넣음. 
configurations {
    querydsl.extendsFrom compileClasspath
}
//어노테이션 프로세서와 맞물려서 빌드시 생성
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

//여기가 안되면 삽질을 좀 해야합니다.
  • querydsl은 빌드 시 Q파일을 만들어 쿼리를 관리
  • 위의 설정은 해당 Q파일을 build/generated/프로젝트와동일한구조 로 만듬.
  • 일반적으로 Q파일은 깃헙에 올리면 안되는데(설정,버전에 따른 차이가 있기 때문) 이와 같이 하면 보통 build 디렉토리는 ignore되어 있기 때문에 자동으로 ignore.
  1. 예시 엔티티

@Entity
@Getter
@NoArgsConstructor
public class Hello {

    @Id
    @GeneratedValue
    private Long id;
}
  1. 빌드

위의 빌드 작업목록에 compileQuerydsl이 존재

  • ./gradlew compileQuerydsl
  • ./gradlew build
    시에도 Q파일 생성 가능
  1. Q파일 및 테스트 검증
@Generated("com.querydsl.codegen.EntitySerializer")
public class QHello extends EntityPathBase<Hello> {

    private static final long serialVersionUID = -229139972L;

    public static final QHello hello = new QHello("hello");
	//자기 자신을 가지고 있음

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QHello(String variable) {
        super(Hello.class, forVariable(variable));
    }

    public QHello(Path<? extends Hello> path) {
        super(path.getType(), path.getMetadata());
    }

    public QHello(PathMetadata metadata) {
        super(Hello.class, metadata);
    }

}
    @Test
    void contextLoads() {
        Hello hello = new Hello();
        em.persist(hello);

        JPAQueryFactory query = new JPAQueryFactory(em);
        QHello qHello = QHello.hello;

        //쿼리와 관련된건 Q타입으로 작성함. 
        Hello result= query
                .selectFrom(qHello)
                .fetchOne();

        assertThat(result).isEqualTo(hello);
        assertThat(result.getId()).isEqualTo(hello.getId());
    }

라이브러리 살펴보기

  1. querydsl-apt : Q파일을 생성하는
  2. querydsl- jpa : querydsl은 JPA뿐만 아니라 다양한 쿼리들을 같은 경험으로 사용할 수 있도록 지원해줌.

h2, jpa, db 설정, 예제 도메인 설계

이전과 동일!

Querydsl 기본 문법

JPQL vs Querydsl

  1. @BeforeEach 초기화

//QueryBasicTest.class

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }
  • 기본적인 세팅
  1. JPQL을 사용한 멤버 조회

//멤버1 찾기
@Test
public void startJPQL() {
    
	String qlString = "select m from Member m " +
                "where m.username=:username";

    Member findMember = em.createQuery(qlString, Member.class)
                .setParameter("username", "member1")
                .getSingleResult();
    Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
    }

  • 쿼리를 String으로
  • 파라미터 바인딩하는 모습
  1. Querydsl을 사용한 멤버 조회


    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m"); //어떤 Q멤버인지 별칭

        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1")) //파라미터 바인딩을 안해도, jdbc prepare statement로 바인딩을 해줌.
                .fetchOne();

        Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");

    }

  • JPAQueryFactory를 사용. 초기화 시 entity managaer를 제공해줘야 함.
  • 현재 Member의 Q타입인 Q멤버는 없음. 따라서, compileQuerydsl하여 Q타입을 생성해줘야함.
  • 현재 사용할 QMember에 별칭을 붙여서 사용중. 사실 QMember안의 static으로 가지고 있는 객체를 사용해도 됨.
  • 이후 일반적인 쿼리의 형태로 사용이 가능.

Querydsl의 장점
1) 문자열로 쿼리를 만드는 것이 아니고, Q타입을 생성해서 자바 코드로 처리하기 때문에, 컴파일 에러를 활용 가능. 만약 쿼리문을 잘못 짠 경우, 컴파일러가 잘못 짰다고 알려줌.
2) 코드 어시스턴스 활용 가능. 뭐 있을것 같은데..? like 있나..? 같은 경우
3) 파라미터 바인딩을 해줌. where절에서 보이는 것이 문자열로 처리하는 것이 아닌, jdbc prepare statement를 통해서 하는 것이기 떄문에, 파라미터 바인딩이 알아서 처리되는 것.

  1. JPAQueryFactory를 필드 타입으로 사용

//QueryBasicTest.class

    @Autowired
    EntityManager em;

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em); 
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
	
	...

	}

  • em 자체가 멀티 쓰레딩 환경에서 트랜잭션에 바운딩되어 분배되기 때문에, 동시성 문제를 고려되어 설계됨.
  • 따라서 위처럼 필드로 사용 가능.

기본 Q-Type 활용

  1. Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m");
QMember qMember = QMember.member;
  • QMember에 static final로 선언되어 있음.
  • 더 줄이려면 아예 static import 하는 법.

import static study.querydsl.domain.QMember.*;

...
//Test Method
   Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();
  • Querydsl은 사실상 JPQL의 빌더라고 보면 됨.
  • 쿼리 말고 만약 JPQL을 보고 싶다면,
  • jpa.properties.hibernate.use_sql_comments = true
  • 별칭을 통해서 jpql의 alias도 변경 가능 (같은 테이블을 조인하거나 하는 경우에 사용)
    	```java
    	 QMember qmember = new QMember(“m1”); 
    		...
    	```

검색 조건 쿼리

  1. Querydsl의 검색 조건 쿼리
@Test
public void search() {
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1")
                        .and(member.age.eq(10)))
                .fetchOne();        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • and로 연결되는 모습.
  • 이외의 JPQL이 제공하는 모든 검색 조건 제공
  1. 검색 기능

member.username.eq("member1") // username == "member1"
member.username.ne("member1") // username != "member1"
member.username.eq("member1").not() // username != "member1"

member.username.isNotNull()

member.age.in(10,20) //age in 10,20
member.age.notIn(10,20) // age not in 10,20
member.age.between(10,30)

member.age.goe(30) // age >= 30, greater or equal
member.age.gt(30) // age>30 , greater than
member.age.loe(30) // age<=30, low or eqaul
member.age.lt(30) // age<30 , low than

member.usename.like("member%") //like검색
member.username.contains("member") //like %member% 검색
member.username.startsWith("member") // like "member%" 검색

...
  • 아 이거 있지 않을까? 하면 나옴
  1. And
    @Test
    public void searchAndParam() {
        Member findMember = queryFactory
                .selectFrom(member)
                .where(
                        member.username.eq("member1"),
                        member.age.eq(10)
                )
                .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • 위처럼 체이닝 하는 방식도 가능하지만,
  • and인 경우 ,로 연결 가능
  • 이 경우 중간에 null이 들어가는 경우 무시하기 때문에, 동적 쿼리 생성에 용이

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    - 결과가 없으면 null
    - 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 쿼리를 같이 날려, total count 쿼리 추가
  • fetchCount() : count 쿼리로 변경, count 수만 조회
    @Test
    public void resultFetch() {
        List<Member> fetch = queryFactory
                .selectFrom(member)
                .fetch();

        Member fetchOne = queryFactory
                .selectFrom(member)
                .fetchOne();


        //.limit(1).fetchOne()과 동일
        Member oneMember =
                queryFactory.selectFrom(QMember.member)
                .fetchFirst();

        //패아장이 나감 -> 카운트 쿼리 + 데이터 쿼리 같이나감
        QueryResults<Member> results = queryFactory
                .selectFrom(QMember.member)
                .fetchResults();

        results.getTotal();
		//content를 가져가야함.
        List<Member> content = results.getResults();

        long total = queryFactory.selectFrom(member)
                .fetchCount();
        
    }
  • fetchResult같은 경우, 페이징 쿼리가 복잡해지면 total을 가져오는 쿼리와 content를 가져오는 쿼리가 달라질 수 있음. 만약 복잡하고 성능이 중요한 경우, 이걸 쓰면 안되고 쿼리를 두방 날려야 함.

  • 이렇게 카운트 쿼리가 나가고, id만 select.

정렬


    /**
     * 회원 정렬 순서
     * 1. 회원 나이 내림차순 (Desc)
     * 2. 회원 이름 오름차순 (asc)
     * 단 2에서 회원 이름이 없으면 마지막에 출력 (Nulls last)
     */
    @Test
    public void sort() {

        em.persist(new Member(null,100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();

        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member memberNull = result.get(2);

        assertThat(member5.getUsername()).isEqualTo("member5");
        assertThat(member6.getUsername()).isEqualTo("member6");
        assertThat(memberNull.getUsername()).isNull();
    }

페이징

    @Test
    public void paging1() {
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1) //앞에 몇개 스킵할거야? -> 1은 하나 스킵할거야. (0부터 시작)
                .limit(2)
                .fetch();


        assertThat(result.size()).isEqualTo(2);
    }

    @Test
    public void paging2() {
        QueryResults<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1) //앞에 몇개 스킵할거야? -> 1은 하나 스킵할거야. (0부터 시작)
                .limit(2)
                .fetchResults();

        assertThat(result.getTotal()).isEqualTo(4);
        assertThat(result.getLimit()).isEqualTo(2);
        assertThat(result.getOffset()).isEqualTo(1);
        assertThat(result.getResults().size()).isEqualTo(2);
        
    }
  • 실무에서는 count 쿼리를 따로 분리해서 써야 할 수도 있음. count 쿼리는 좀 단순하게 작성할 수 있는 경우가 있기 때문에 성능을 위해서. 왜냐면 fetchResults로 하면 count 쿼리에도 where 붙고 그럴 수 있음.

집합

놀라운 템플릿

  • 영한님이 tdd만 쳐서 테스트 코드를 쓰는 법을 알려주셨다.

  • 요렇게 해놓으면, tdd만 쳤을 때 테스트 코드 틀을 쓸 수 있다는 것.
  1. 집합
    
@Test    
public void aggregation() {
        List<Tuple> result = queryFactory
                .select(
                        member.count(),
                        member.age.sum(),
                        member.age.avg(),
                        member.age.max(),
                        member.age.min()
                )
                .from(member)
                .fetch();

        Tuple tuple = result.get(0);

        assertThat(tuple.get(member.count())).isEqualTo(4);
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
        assertThat(tuple.get(member.age.avg())).isEqualTo(25);
        assertThat(tuple.get(member.age.max())).isEqualTo(40);
        assertThat(tuple.get(member.age.min())).isEqualTo(10);

        //Tuple로 조회하는 이유 : 데이터가 여러 타입으로 들어오기 때문. 실무에서 많이 쓰지는 않고, DTO로 직접 뽑아오는 방법을 많이 씀.

    }

  • Tuple로 조회되는 이유 : 현재 데이터가 여러 타입으로 돌아오고 잇기 때문.
  • 실무에서 많이 쓰이지는 않고, DTO로 직접 뽑아옴.
  1. groupBy

    /**
     * 팀의 이름과 각 팀의 평균 연령을 구해라
     */
    
@Test    
public void group() throws Exception {

       	List<Tuple> result = queryFactory
                .select(team.name, member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name)
                .fetch();

        Tuple teamA = result.get(0);
        Tuple teamB = result.get(1);

        assertThat(teamA.get(team.name)).isEqualTo("teamA");
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);

        assertThat(teamB.get(team.name)).isEqualTo("teamB");
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);
    }

  • 마찬가지로 튜플로 조회 가능.
  • sql의 groupBy처럼 조회가 됨.
  1. Having
    @Test
    public void havingTest() throws Exception {

        em.persist(new Member("test",40));
        em.persist(new Member("test",50));


        List<Tuple> fetch = queryFactory
                .select(member.age,member.count())
                .from(member)
                .groupBy(member.age)
                .having(member.age.gt(30))
                .fetch();

        for (Tuple tuple : fetch) {
            System.out.println(tuple);
        }

        Tuple targetAge = fetch.get(0);
        assertThat(targetAge.get(member.age)).isEqualTo(40);
    }
	...
  • 대충 요런 식으로 짜봤다.
  • 이렇게 되면 위에 beforeeach에서 40살인 회원을 넣어놨기 때문에,
  • (40,2), (50,1) 의 결과가 나온다.
  • 튜플이 궁금해서 찍어봤더니, python tuple처럼 (40,2), (50,1) 이렇게 나온다. 여러 타입을 한번에 넣는 구조인 것 같다.
profile
SKKU Humanities & Computer Science

0개의 댓글