순수 JPA 리포지토리와 Querydsl
순수 JPA 리포지토리로 Querydsl을 사용해보자
@Repository
public class MemberJpaRepository {
private final EntityManager em; // 순수 JPA 접근
private final JPAQueryFactory queryFactory; // querydsl 사용 위함
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> findAll_Querydsl(){
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByUsername(String username){
return em.createQuery("select m from Member m where m.username =:username", Member.class)
.setParameter("username", username)
.getResultList();
}
public List<Member> findByUsername_Querydsl(String username){
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {
@Autowired
EntityManager em;
@Autowired
MemberJpaRepository memberJpaRepository;
@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_Querydsl();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberJpaRepository.findByUsername_Querydsl("member1");
assertThat(result2).containsExactly(member);
}
}
참고로 JPAQueryFactory 를 빈으로 등록해서 주입받아 사용해도 된다.
@SpringBootApplication
public class QuerydslApplication {
public static void main(String[] args) {
SpringApplication.run(QuerydslApplication.class, args);
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
}
public MemberJpaRepository(EntityManager em, JPAQueryFactory queryFactory) {
this.em = em;
this.queryFactory = queryFactory;
}
// = @RequiredArgsConstructor
이렇게 빈으로 등록하면 같은 객체를 모든 멀티쓰레드에서 사용하는데, 괜찮을까?
→ 괜찮다.
JPAQueryFactory에 대한 동시성 문제는 엔티티 매니저에 의존한다.
엔티티매니저가 스프링을 엮어서 쓰면 동시성 문제와 관계 없이 트랜잭션 단위로 따로따로 분리돼서 동작하게 된다.
스프링에서는 진짜 영속성 컨텍스트 매니저가 아니라 가짜 프록싱 객체를 주입한다. 트랜젝션 단위로 다 다른데 바인딩 되도록 라우팅만 해준다.
동적 쿼리와 성능 최적화 조회 - Builder 사용
@Data
public class MemberSearchCondition {
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
MemberJpaRepository
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.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
@Test
public 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");
// 만약에 조건이 없으면 모든 데이터를 가져온다. 웬만해서는 기본 조건이나 limit 가 있는게 좋다. 가급적이면 페이징 쿼리가 같이 나가야 한다.
List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);
assertThat(result).extracting("username").containsExactly("member4");
}
동적 쿼리와 성능 최적화 조회 - Where 절 파라미터 사용
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
만약 DTO가 아닌 엔티티를 조회하는 것처럼 select projection 이 달라져도 조건(메소드)들을 재사용 할수있다는 장점이 있다.
아래와 같이 조건을 조립할 수도 있다.
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
// ageGoe(condition.getAgeGoe()),
// ageLoe(condition.getAgeLoe()))
ageBetween(condition.getAgeLoe(), condition.getAgeGoe()))
.fetch();
}
private BooleanExpression ageBetween(int ageLoe, int ageGoe){
return ageGoe(ageLoe).and(ageGoe(ageGoe));
}
조회 API 컨트롤러 개발
로컬에서 메인 소스코드를 Tomcat으로 돌릴 때랑 테스트 케이스를 돌릴 때랑 프로파일을 분리하도록 하자.
프로파일 설정
applicatinon.yml
spring:
profiles:
active: local # 추가
테스트는 src/test/resources
밑에 application.yml 을 복사하고 active 를 test 로 수정한다.
그럼 test 를 실행 시에는 active: test 가 되어 initMember가 실행이 안되고
애플리케이션 실행 시에만 실행 된다.
controller 폴더에 initMember 파일을 만든다.
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember { // 스프링부트로 메인 클래스를 실행하면 로컬이라는 프로파일로 실행이 된다. 그럼 이 클래스가 동작한다.
private final InitMemberService initMemberService;
@PostConstruct
public void init(){
initMemberService.init();
}
@Component
static class InitMemberService {
@PersistenceContext
private EntityManager em;
@Transactional
public void init() { // PostConstructor 하는 부분과 트랜잭션 동작하는 부분은 분리를 해줘야 한다.
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));
}
}
}
}
MemberController
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.search(condition);
}
}
데이터들이 필터링되어 condition에 따라 출력되는 것을 확인할 수 있다.
인프런 - 실전! Querydsl
https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84
'BACK > JPA' 카테고리의 다른 글
[JPA] JPA 소개, 사용 이유 (0) | 2024.03.30 |
---|---|
[JPA] 실무 활용 - 스프링 데이터 JPA 와 Querydsl (1) | 2024.03.15 |
[JPA] Querydsl 중급 문법 (프로젝션, 동적 쿼리, 벌크 연산, SQL function) (1) | 2024.03.15 |
[JPA] Querydsl 기본 문법 (검색, 조회, 정렬, 페이징, 집합, 조인등) (0) | 2024.03.15 |
[JPA] Querydsl 프로젝트 환경설정 - 설정과 검증 (0) | 2024.03.15 |