dynamic query
서버사이드로 주로 사용하는 framework는 nestjs , gin을 사용하지만 클라이언트쪽에서 spring으로 개발하기를 원해서 spring으로 개발하고 있다. typeorm이나 gorm을 사용할 때는 dynamic query를 사용하기 편했는데 spring은 mybatis 밖에 사용해본 적이 없어서 jpa를 제대로 사용하기 위해서 찾아보게 되었다.
계기
@EntityGraph(
attributePaths = {"community" , "likes"}
)
List<CommunityCommentEntity> findByUser(UserEntity user , Pageable pageable);
JpaRepository EntityGraph를 사용해서 pagination을 하고 fetch를 했을 경우 결과 전체를 가져와서 pagination을 해주기 떄문에 다른 방법이 필요했다.
warn message : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
specification & critrefia || querydsl
선택지는 specification & critrefia || querydsl 두개였는데
querydsl를 선택하지 않은 이유는 package를 impelement를 하고 설정도 따로 해줘야 했기 떄문에 Jpa에 내장된 JpaSpecificationExecutor를 그냥 쓰기로 했다.
사용방법
UserRepository.java
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository
public class UserRepository extends JpaRepository<UserEntity , UUID> , JpaSpecificationExecutor<UserEntity> {
}
repository에 JpaSpecificationExecutor<Entity> 상속받아준다.
UserSpecification.java
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
public class UserSpecification {
public static Specification<UserEntity> userIdAndDeviceName(String word) {
return new Specification<UserEntity>() {
@Override
public Predicate toPredicate(Root<UserEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// join할 table의 변수명 , left | right | inner
Join<UserEntity , DeviceEntity> devices = root.join("devices" , JoinType.LEFT);
/*
조건에 따라서 dynamic하게 query를 날릴 수 있다.
if(condition) {
returrn builder.and(
builder.equal(root.get("userId") , word)
)
}
*/
return builder.or(
// ex) iphone이라는 단어를 검색했을 때 userId와 devices.name에 iphone가 포함된 row를 찾습니다.
builder.like(root.get("userId") , "%" + word + "%"),
builder.like(devices.get("name") , "%" + word + "%")
);
}
};
}
}
devices table에 fk로 user table에 fk가 걸려있는 경우 위와 같이 join할 수 있다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
public Page<UserEntity> users(String word , int page , int count) {
Page<UserEntity> users = this.userRepository.findAll(
UserSpecification.userIdAndDeviceName(word),
PageRequest.of(page, count , Sort.by("createdAt").descending())
);
return users;
}
Jpa에 정의되어 있는 findAll의 첫번째 파라미터로 spec을 넣어주고 페이징을 할 경우 Pageable을 두번쨰 파라미터에 넣어주면 된다.
select * from users u
left outer join devices d
on u.id = d.users
where u.user_id like '%iphone%' OR d.name like '%iphone%'
위 처럼 query를 날릴 수 있다.