2021년이 가기 전 Mybatis 인생을 탈피하고자(!)
인프런에 있는 김영한님의 강의 두개를 켠왕했다( 실전 jpa 강의, querydsl 강의)
(2021년 마지막 날 태우는 중.jpg)
querydsl은 jpa 동적쿼리인데 이걸 공부한 이유는 이전에 jpa로 모듈을 짜놨는데 테이블 이름이 계속 변하는(!) 엄청난 상황을 만나 결국 mybatis를 이용해 프로그램을 다시 짰던 트라우마(?)가 있어서
혹시 jpa 전용 동적쿼리를 쓰면 괜찮지 않을까 해서 공부해보았다
(결론부터 얘기하면 querydsl도 entity 기반이여서 저 경우에는 그냥 JDBC나 mybatis 사용하는게 나을 것 같다)
비록 궁금증을 해결하지는 못했지만 강의를 들으며 JPA와 querydsl의 편리함에 대해 알 수 있었다
필요한 부분을 정리해서 스니펫으로 활용해보도록 하겠다 👩🏫
이 곳에는 간단하게만 적을 것이고 좀 더 깊은 내용에 대해서는 각각 따로 정리할 것이다
fetchJoin
@ManyToOne(fetch = FetchType.LAZY)으로 지연로딩을 하면 객체를 조회할 때 join된 객체를 바로 불러오는 것이 아니라 프록시 객체로 대체를 해놓고,
필요할 때 직접 select 해오게 된다
그래서 연관객체를 한 번에 불러오고 싶다면 fetch join을 하는 것이 성능 최적화하는데 도움이 된다(1 + N 문제 해결)
✍ jpql(Spring Data JPA)
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
✍ Querydsl
@Autowired
EntityManager em;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
...
//fetch join 할 때는 초기화 하고 시작하는 것이 좋음
em.flush();
em.clear();
//기본적으로는 team이 Lazy로 되어있다
Member findMember = queryFactory.selectFrom(QMember.member)
.join(member.team, Qteam.team).fetchJoin()
.where(QMember.member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); //초기화 된 엔티티 여부
assertThat(loaded).as("페치 조인 적용").isTrue();
DTO 반환
✍ Spring Data JPA
@Query("select new com.zzarbttoo.datajpa.dto.MemberDTO(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDTO> findMemberDTO();
✍ Querydsl
1) setter
List<MemberDTO> result = queryFactory.select(
Projections.bean(MemberDTO.class,member.username,member.age))
.from(member)
.fetch();
2) field
List<MemberDTO> result = queryFactory.select(Projections.fields(MemberDTO.class
, member.username
, member.age))
.from(member)
.fetch();
3) constructor
List<MemberDTO> result = queryFactory.select(Projections.constructor(MemberDTO.class
, member.username, member.age))
.from(member)
.fetch();
4) field명이 다를 때 DTO select
QMember memberSub = new QMember("memberSub");
List<UserDTO> result = queryFactory.select(Projections.fields(UserDTO.class
, member.username.as("name") //다른 field명을 직접 명시해줘야한다
//, member.age))
, ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max()) //서브쿼리를 이용해서 출력한다 가정
.from(memberSub), "age") //두번째 파라미터를 age로 별칭을 만들었다
))
.from(member)
.fetch();
5) Query Projection
@Data //기본 생성자는 안만들어준다
@NoArgsConstructor
public class MemberDTO {
private String username;
private int age;
@QueryProjection //QueryProjection annotation 생성 후 querydsl compile 해줘야 한다
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
}
이렇게 DTO 생성자에 QueryProjection annotation 생성한 후 querydsl compile을 하고
다음과 같이 사용 가능
List<MemberDTO> result = queryFactory.select(new QMemberDTO(member.username, member.age))
.from(member)
.fetch();
Paging
✍ JPA
public List<Member> findByPage(int age, int offset, int limit){
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset) //어디서부터 가져올 것인가
.setMaxResults(limit) //몇개 넘길 것인가
.getResultList();
}
✍ Spring Data JPA
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
참고로 PageRequest 는 다음과 같이 작성도 가능하다(Test 할 때 유용~)
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
✍ Querydsl
List<MemberTeamDTO> content = 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())
)
//.orderBy()
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Member> countQuery = queryFactory.select(member).from(member)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);//.fetchCount();
//return new PageImpl<>(content, pageable, total);
//count를 생략할 수 있을 때 생략하도록 한다
//return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
subQuery
✍ Querydsl
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory.selectFrom(member)
.where(member.age.goe(select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
List<Member> result = queryFactory.selectFrom(member)
.where(member.age.in(select(memberSub.age)
.from(memberSub).where(memberSub.age.gt(10))
))
.fetch();
List<Tuple> result = queryFactory.select(member.username,
select(memberSub.age.avg()).from(memberSub))
.from(member)
.fetch();
from 절 서브쿼리 해결방안
1. 서브 쿼리 -> join(가능한 상황이 있고 불가능한 상황이 있다)
2. 어플리케이션에서 쿼리를 2번 분리해서 실행한다
3. nativaSQL을 사용한다
정말 복잡한 쿼리는 쪼개서 하면 더 나을 수 있다
SQL은 정말 최소한의 데이터를 호출/grouping 하는 것에 집중해서 쓰면 된다
bulkUpdate
✍ JPA
public int bulkAgePlus(int age){
int resultCount = em.createQuery("update Member m set m.age = m.age + 1 " +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
✍ Spring Data JPA
1) flush, clear
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
라고 repository에 선언하고 아래와 같이 호출
int resultCount = memberRepository.bulkAgePlus(20);
entityManager.flush();
entityManager.clear();
2) modifying 설정
@Modifying(clearAutomatically = true) //자동으로 clear 과정을 해준다
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
혹은 @Modifying 설정을 해서 자동 clear을 할 수 있다
int resultCount = memberRepository.bulkAgePlus(20);
✍ Querydsl
long count = queryFactory.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28)) //28 이하면 이름을 비회원으로 바꿈
.execute();
//bulk 연산에서는 초기화 필요(영속성 컨텍스트 날림)
em.flush();
em.clear();
queryFactory.update(member)
//.set(member.age, member.age.add(1)) //마이너스 하고 싶으면 add(-1)
.set(member.age, member.age.multiply(2)) //마이너스 하고 싶으면 add(-1)
.execute(); //모든 회원의 나이 한살 더하기
queryFactory.delete(member)
.where(member.age.gt(18))
.execute();
dynamic query
✍ JPA
em.createQuery("select m from Member m where m.username = :username and m.age > :age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
✍ Spring Data JPA
@Query("select m from Member m where m.username = :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
✍ Querydsl
1) BooleanBuilder
@Test
public void dynamicQuery_BooleanBuilder(){
//검색 조건
String usernameParam = "member1";
//Integer ageParam = 10;
Integer ageParam = null;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
//검색 조건을 추가
if (usernameCond != null){
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null){
builder.and(member.age.eq(ageCond));
}
return queryFactory.selectFrom(member)
.where(builder)
.fetch();
}
2) WhereParam
@Test
public void dynamicQuery_WhereParam(){
//검색 조건
String usernameParam = "member1";
//Integer ageParam = 10;
Integer ageParam = null;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory.selectFrom(member)
//.where(usernameEq(usernameCond), ageEq(ageCond))
.where(allEq(usernameCond, ageCond))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond == null ? null : member.username.eq(usernameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
private BooleanExpression allEq(String usernameCond, Integer ageCond){
return usernameEq(usernameCond).and(ageEq(ageCond));
}
concat
✍ Querydsl
//{username}_{age}
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
distinct
queryFactory.select(member.username).distinct()
.from(member)
.fetch();
수업을 들으며 더 많은 내용을 들었지만 정말 많이 사용할 내용은
이정도일 것 같아서 이렇게 정리했다
확실히 책으로 공부하는것보다 빠르고 재밌게 공부할 수 있던 것 같다
(영한님 너무 깜찍하시다 재채기마저도 재밌게 하신다)
책도 예전에 사놔서 더 깊은 내용은 차차 공부해나가면 될 듯 하다