Spring Framework/[인프런] 실전! QueryDSL

Chapter 04. 실무 활용 - 순수 JPA와 Querydsl

계란💕 2023. 5. 6. 16:27

순수 JPA repository와 Querydsl

 

  Ex) 순수 JPA repository (Spring Data JPA가 아닌 쌩 JPA)

<hide/>
@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }

    public void save(Member member) {
        em.persist(member);
    }

    public Optional<Member> findById(Long id) {
        Member findMember = em.find(Member.class, id);
        return Optional.ofNullable(findMember);
    }

    public List<Member> findAll() {
        return em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
    }

    public List<Member> findByUsername(String username) {
        return em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
            .setParameter("username", username)
            .getResultList();
    }
}
  • Spring Data JPA에서 기본으로 제공하는 메서드 findById(), findAll(), findByUsername()를 직접 구현한다. 
  • em.createQuery() 안에 JPQL로 SELECT 쿼리를 작성한다. 

 

 

  •  순수 JPA repositoryTest
<hide/>
@Test
public void basicTest(){
    Member member = new Member("member1", 10);
    memberJpaRepository.save(member);

    Member findMember = memberJpaRepository.findById(member.getId()).get();
    assertThat(findMember).isEqualTo(member);

    List<Member> result1 = memberJpaRepository.findAll();
    assertThat(result1).containsExactly(member);

    List<Member> result2 = memberJpaRepository.findByUsername("member1");
    assertThat(result2).containsExactly(member);
}

 


  Ex) 순수 JPA repository - querydsl 추가

<hide/>
private final QMember member = QMember.member;

public List<Member> findAll_Querydsl() {
    return queryFactory.selectFrom(member).fetch();
}

public List<Member> findByUsername_Querydsl(String username) {
    return queryFactory.selectFrom(member)
        .where(member.username.eq(username))
        .fetch();
}
  • repository에내용을 추가
  • queryFactory 를 이용해서 ".select()" 같은 메서드를 활용해서 반환하는 메서드를 작성할 수 있다. 

 

 

  •  순수 JPA repositoryTest - querydsl 테스트 추가
<hide/>
@Test
public void basicQuerydslTest() {
    Member member = new Member("member1", 10);
    memberJpaRepository.save(member);

    Member findMember = memberJpaRepository.findById(member.getId()).get();
    assertThat(findMember).isEqualTo(member);

    List<Member> result1 = memberJpaRepository.findAll_Querydsl();
    assertThat(result1).containsExactly(member);

    List<Member> result2 = memberJpaRepository.findByUsername_Querydsl("member1");
    assertThat(result2).containsExactly(member);

}

 

 

  cf) 다음과 같이 factory를 빈으로 등록해서 주입받아 사용할 수도 있다. 

<hide/>
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
	return new JPAQueryFactory(em);
}
  • 동시성 문제: 여기에서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당한다. 

 


동적 쿼리와 성능 최적화 조회 - Builder 사용

  • MemberTeamDto
    • @QueryProjection
      • Spring Data JPA에서 제공하는 애너테이션이다.
      • 엔티티가 아닌 DTO 클래스에 적용한다. DTO의 생성자에 애너테이션을 붙여야한다.
      • 생성자를 통해 DTO를 조회하는 방법과 함께 사용된다.
      • 엔티티와 DTO 간의 매핑을 위해 사용한다
<hide/>
@Data
public class MemberTeamDto {

    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}

 

  • SearchCond
<hide/>
@Data
public class MemberSearchCondition {

    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;

}

 


NullPointerException - 스프링 컨테이너에 존재하는 Repository가 아닌 새로운 Repository를 선언한 경우

 

  • 오류: search 의 return 문에서 NullPointerException이 발생한다. 
  • 원인: Test 클래스에서 MemberJpaRepository를 선언할 때, 기존에 Spring 컨테이너에 있는 MemberJpaRepository 를 사용해야한다.
    • 그런데  이를 새로운 인스턴스 new MemberJpaRepository로 선언해서 사용했기 때문에 에러가 난다.
  • 해결: 이미 Spring Context 안에 이미 생성된 repository를 사용한다. @Autowired를 이용해서 주입해준다.
    • Autowired를 사용하면 싱글톤으로 미리 생성된 빈이 자동으로 주입되므로 문제가 해결된다. 
<hide/>
@SpringBootTest
@Transactional
public class QuerydslBasicTest {

   @Autowired
    MemberJpaRepository memberJpaRepository;

//    오류: MemberJpaRepository memberJpaRepository = new MemberJpaRepository(em);
 
    @Test
    void searchTest(){
        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);
        MemberSearchCondition condition = new MemberSearchCondition();
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");
        List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);
        assertThat(result).extracting("username").containsExactly("member4");
    }
}

 

<hide/>
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
    BooleanBuilder builder = new BooleanBuilder();
    if (hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getUsername()));
    }
    if (hasText(condition.getTeamName())) {
        builder.and(team.name.eq(condition.getTeamName()));
    }
    if (condition.getAgeGoe() != null) {
        builder.and(member.age.goe(condition.getAgeGoe()));
    }
    if (condition.getAgeLoe() != null) {
        builder.and(member.age.loe(condition.getAgeLoe()));
    }
    return queryFactory
        .select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name))
        .from(member)
        .leftJoin(member.team, team)
        .where(builder)
        .fetch();
}

 


동적 쿼리와 성능 최적화 조회 - Where 절 파라미터 사용

  • search
<hide/>
private BooleanExpression usernameEq(String username) {
    return isEmpty(username) ? null : member.username.eq(username);
}

private BooleanExpression teamNameEq(String teamName) {
    return isEmpty(teamName) ? null : team.name.eq(teamName);
}

private BooleanExpression ageGoe(Integer ageGoe) {
    return ageGoe == null ? null : member.age.goe(ageGoe);
}

private BooleanExpression ageLoe(Integer ageLoe) {
    return ageLoe == null ? null : member.age.loe(ageLoe);
}

public List<MemberTeamDto> search(MemberSearchCondition condition) {
    return queryFactory
        .select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name))
        .from(member)
        .leftJoin(member.team, team)
        .where(
            teamNameEq(condition.getTeamName()),
            usernameEq(condition.getUsername()),
            ageGoe(condition.getAgeGoe()),
            ageLoe(condition.getAgeLoe())
        )
        .fetch();
}

 

  • test 코드는 앞의 예제와 동일하다. 

 


조회 API 컨트롤러 개발

  • yml 파일 설정
    • test폴더 안에 resources 폴더를 만들고 아래 yml 파일과 똑같지만 active 속성만 "active: test" 로 바꿔서 설정한다. 
    • 분리하면 main 소스코드와 테스트 소스 코드 실행 시 프로파일을 분리할 수 있다. 
<hide/>
spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/querydsl
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        show_sql: true
        format_sql: true
        use_sql_comments: true
  logging.level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace

  profiles:
    active: local

 

 

  • InitMember
    • @Profile(value): 스프링 부트 애플리케이션의 런타임 환경을 관리할 수 있다. 
      • value: 프로파일이 value와 일치하는 경우 해당 영역 안에서 명시한 스프링 빈을 등록한다. 
    • @PostConstructor
      • 빈으로 등록된 어떤 클래스의 메서드에 @PostConstructor를 붙일 수 있다. 
      • 인스턴스가 만들어진 다음에 특정 메서드가 자동으로 호출되도록 한다. 
<hide/>
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {

    private final InitMemberService initMemberService;

    @PostConstruct
    public void init() {
        initMemberService.init();
    }

    @Component
    static class InitMemberService {

        @PersistenceContext
        EntityManager em;

        @Transactional
        public void init() {
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            em.persist(teamA);
            em.persist(teamB);
            for (int i = 0; i < 100; ++i) {
                Team selectedTeam = i % 2 == 0 ? teamA : teamB;
                em.persist(new Member("member" + i, i, selectedTeam));
            }
        }
    }
}

 

 

  • controller
<hide/>
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition){
    return memberJpaRepository.search(condition);
}

 

  • postman
    • GET http://localhost:8080/v1/members?teamName=teamB&ageGoe=31&ageLoe=35
    • 위와 같이 teamName과 age에 대한 조건을 query Param으로 넣어준다. 

 

 

  Note) 실행 결과

  • 해당 조건에 맞는 member가 출력된다. 

 

 


BeanCreationException

<hide/>

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'initMember': Invocation of init method failed; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call

 

  • 오류: Boot 가 안 뜬다. 
  • 원인: init() 메서드 위에 @Transactional 을 안 붙였다. 
  • 해결

Title

  • contents

출처 -  https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84