Back-End/Spring
[Spring][일정 관리 앱 만들기] CRUD 생성 - QueryDsl 쿼리문 코드 작성
hampangee
2025. 5. 24. 18:38
이전에 이어서 이제 내가 원하는 API를 구현할 차례이다.
전체 일정 조회를 하는 기능을 추가하려고 하는데 수정일과 작성자Id를 선택으로 받아 null인지 아닌지 체크하여 동적쿼리를 작성해야 한다.
나는 일정 페이지에서 -내가 작성한 OO일 일정들-을 보고싶어서 아래 API를 작성한 것이기 때문에 일치 여부를 판단하는 쿼리를 작성할 것이다.
저번에 구현체 클래스에 로직을 작성하면 된다고 했다.
그래서 구현체 클래스에 아래와 같은 쿼리문을 작성했다.
package com.sparta.schedule.schedule.repository;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.sparta.schedule.schedule.entity.QSchedule;
import com.sparta.schedule.schedule.entity.Schedule;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class ScheduleRepositoryCustomImpl implements ScheduleRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Schedule> findSchedulesByUpdatedAtAndWriterId(LocalDate updatedAt, Long writerId) {
QSchedule schedule = QSchedule.schedule;
BooleanBuilder builder = new BooleanBuilder();
if (updatedAt != null) {
builder.and(schedule.updatedAt.eq(updatedAt));
}
if (writerId != null) {
builder.and(schedule.user.id.eq(writerId));
}
return queryFactory.selectFrom(schedule)
.where(builder)
.fetch();
}
}
일단 이렇게 하고 테스트하러 고고
파라미터를 넣든 말든 원하는 값이 잘 나온다.
전체 코드
Controller
@RestController
@RequestMapping("/schedules")
@RequiredArgsConstructor
public class ScheduleController {
private final ScheduleService scheduleService;
@PostMapping
public ResponseEntity<ScheduleDto> createSchedule(@RequestBody CreateScheduleRequestDto request) {
ScheduleDto response = scheduleService.createSchedule(
request.getTitle(),
request.getContents(),
request.getWriterId()
);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<ScheduleDto> getSchedule(@PathVariable Long id) {
ScheduleDto response = scheduleService.getSchedule(id);
return new ResponseEntity<>(response, HttpStatus.OK);
}
@GetMapping
public ResponseEntity<List<ScheduleDto>> getSchedules(@RequestParam(value = "updatedAt", required = false)LocalDate updatedAt,
@RequestParam(value = "writerId", required = false) Long writerId) {
List<ScheduleDto> response = scheduleService.getSchedulesByUpdatedAtAndWriterId(updatedAt, writerId);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
Service
@Service
@RequiredArgsConstructor
public class ScheduleService {
private final UserRepository userRepository;
private final ScheduleRepository scheduleRepository;
public ScheduleDto createSchedule(String title, String contents, Long writerId) {
Schedule newSchedule = new Schedule(title, contents);
newSchedule.setUser(userRepository.findByIdOrElseThrow(writerId));
Schedule saved = scheduleRepository.save(newSchedule);
return ScheduleMapper.toDto(saved);
}
public ScheduleDto getSchedule(Long id) {
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(id);
return ScheduleMapper.toDto(findSchedule);
}
public List<ScheduleDto> getSchedulesByUpdatedAtAndWriterId(LocalDate updatedAt, Long writerId) {
List<Schedule> result = scheduleRepository.findSchedulesByUpdatedAtAndWriterId(updatedAt, writerId);
return result.stream()
.map(ScheduleMapper::toDto)
.collect(Collectors.toList());
}
}
Repository
public interface ScheduleRepository extends JpaRepository<Schedule, Long>, ScheduleRepositoryCustom {
default Schedule findByIdOrElseThrow(long id) {
return findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."));
}
}
public interface ScheduleRepositoryCustom {
List<Schedule> findSchedulesByUpdatedAtAndWriterId(LocalDate updatedAt, Long writerId);
}
@Repository
@RequiredArgsConstructor
public class ScheduleRepositoryCustomImpl implements ScheduleRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Schedule> findSchedulesByUpdatedAtAndWriterId(LocalDate updatedAt, Long writerId) {
QSchedule schedule = QSchedule.schedule;
BooleanBuilder builder = new BooleanBuilder();
if (updatedAt != null) {
builder.and(schedule.updatedAt.eq(updatedAt));
}
if (writerId != null) {
builder.and(schedule.user.id.eq(writerId));
}
return queryFactory.selectFrom(schedule)
.where(builder)
.fetch();
}
}
++
아직 페이징을 하고 있지 않지만 예전에 페이지네이션을 적용하는 쿼리를 작성했을땐
AbstractQueryDslRepository 를 만들어 getPageImpl(query, pageable)메소드를 만들고 중복 코드를 줄였었다.
@RequiredArgsConstructor
public class AbstractQueryDslRepository {
protected final JPAQueryFactory queryFactory;
private final EntityManager entityManager;
private Map<String, Querydsl> querydslMap = new HashMap<>();
protected <T> Querydsl getQuerydsl(Class<T> clazz) {
return querydslMap.computeIfAbsent(clazz.getName(), key -> {
PathBuilder<T> builder = new PathBuilderFactory().create(clazz);
return new Querydsl(entityManager, builder);
});
}
protected <T> Page<T> getPageImpl(JPQLQuery<T> query, Pageable pageable) {
long totalCount = query.fetchCount();
List<T> results = getQuerydsl(query.getType()).applyPagination(pageable, query).fetch();
return new PageImpl<>(results, pageable, totalCount);
}
}
이런식으로 구현해서 구현체에 상속하면 계속 JPAQueryFactory나 EntityManager를 가져올 필요가 없어지고 Page처리를 하는 로직이 확 줄어든다.
아래는 예전에 작성했던 Repository이다.
public class UserRepositoryCustomImpl extends AbstractQueryDslRepository implements UserRepositoryCustom {
public UserRepositoryCustomImpl(JPAQueryFactory queryFactory, EntityManager entityManager) {
super(queryFactory, entityManager);
}
@Override
public Page<User> findUsersByRoleId(String roleId, Pageable pageable) {
QUser user = QUser.user;
QUserRole userRole = QUserRole.userRole;
JPQLQuery<User> query = from(user).join(user.userRoles, userRole)
.where(userRole.roleId.eq(roleId))
.select(user);
return getPageImpl(query, pageable);
}
@Override
public Page<User> getUsersByChannelId(String channelId, Pageable pageable) {
// 입력받은 channelId 가 현재 유저가 참여한 채널 중 존재하는 channelId일 경우
QUser user = QUser.user;
QUserChannel userChannel = QUserChannel.userChannel;
JPQLQuery<User> query = from(user)
.join(user.userChannels, userChannel)
.where(userChannel.channelId.eq(channelId));
return getPageImpl(query, pageable);
}
}
이렇게 하면 더 깔끔하게 할 수 있다.
나중에 페이지네이션이 요구사항으로 들어오면 위와 같이 구현할 예정이다.