Skip to content

[BE] issue467: 스터디 목록 조회 커서 기반 페이징 #468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import com.woowacourse.moamoa.study.service.response.StudyDetailResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -24,9 +22,12 @@ public class SearchingStudyController {

@GetMapping
public ResponseEntity<StudiesResponse> getStudies(
@PageableDefault(size = 5) final Pageable pageable
@RequestParam(required = false) final Long id,
@RequestParam(required = false, defaultValue = "") final String createdDate,
@RequestParam(required = false, defaultValue = "5") final int size
) {
final StudiesResponse studiesResponse = searchingStudyService.getStudies("", SearchingTags.emptyTags(), pageable);
final StudiesResponse studiesResponse = searchingStudyService.getStudies("", SearchingTags.emptyTags(), id,
createdDate, size);
return ResponseEntity.ok().body(studiesResponse);
}

Expand All @@ -36,10 +37,13 @@ public ResponseEntity<StudiesResponse> searchStudies(
@RequestParam(required = false, name = "generation", defaultValue = "") final List<Long> generations,
@RequestParam(required = false, name = "area", defaultValue = "") final List<Long> areas,
@RequestParam(required = false, name = "subject", defaultValue = "") final List<Long> tags,
@PageableDefault(size = 5) final Pageable pageable
@RequestParam(required = false) final Long id,
@RequestParam(required = false, defaultValue = "") final String createdDate,
@RequestParam(required = false, defaultValue = "5") final int size
) {
final SearchingTags searchingTags = new SearchingTags(generations, areas, tags);
final StudiesResponse studiesResponse = searchingStudyService.getStudies(title.trim(), searchingTags, pageable);
final StudiesResponse studiesResponse = searchingStudyService.getStudies(title.trim(), searchingTags, id,
createdDate, size);
return ResponseEntity.ok().body(studiesResponse);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ public List<Long> getTagIdsBy(CategoryName name) {
public static SearchingTags emptyTags() {
return new SearchingTags(List.of(), List.of(), List.of());
}

public boolean isEmpty() {
return tags.values()
.stream()
.allMatch(List::isEmpty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.woowacourse.moamoa.study.query.data.StudySummaryData;
import com.woowacourse.moamoa.tag.domain.CategoryName;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -25,34 +27,30 @@ public class StudySummaryDao {
final String excerpt = resultSet.getString("excerpt");
final String thumbnail = resultSet.getString("thumbnail");
final String status = resultSet.getString("recruitment_status");
final LocalDateTime createdDate = resultSet.getObject("created_at", LocalDateTime.class);

return new StudySummaryData(id, title, excerpt, thumbnail, status);
return new StudySummaryData(id, title, excerpt, thumbnail, status, createdDate);
};

private final NamedParameterJdbcTemplate jdbcTemplate;

public Slice<StudySummaryData> searchBy(final String title, final SearchingTags searchingTags, final Pageable pageable) {
public Slice<StudySummaryData> searchBy(final String title, final SearchingTags searchingTags, final Long id,
final LocalDateTime createdAt, final int size) {
final List<StudySummaryData> data = jdbcTemplate
.query(sql(searchingTags, title), params(title, searchingTags, pageable), STUDY_ROW_MAPPER);
return new SliceImpl<>(getCurrentPageStudies(data, pageable), pageable, hasNext(data, pageable));
.query(sql(title, searchingTags, id, createdAt), params(title, searchingTags, id, createdAt, size),
STUDY_ROW_MAPPER);
return new SliceImpl<>(getCurrentPageStudies(data, size), Pageable.ofSize(size), hasNext(data, size));
}

private String sql(final SearchingTags searchingTags, final String title) {
private String sql(final String title, final SearchingTags searchingTags, final Long id,
final LocalDateTime createdAt) {
return "SELECT study.id, study.title, study.excerpt, study.thumbnail, study.recruitment_status, study.created_at "
+ "FROM study "
+ joinTableClause(searchingTags)
+ joinTitleClause(title)
+ filtersInQueryClause(searchingTags)
+ "GROUP BY study.id "
+ "ORDER BY study.created_at DESC "
+ "LIMIT :limit OFFSET :offset ";
}

private String joinTitleClause(final String title) {
if (title.isBlank()) {
return "";
}
return "WHERE UPPER(study.title) LIKE UPPER(:title) ESCAPE '\' ";
+ "FROM study "
+ joinTableClause(searchingTags)
+ whereCondition(id, createdAt, title, searchingTags)
+ "GROUP BY study.id "
+ "ORDER BY study.created_at DESC, id DESC "
+ "LIMIT :limit ";
}

private String joinTableClause(final SearchingTags searchingTags) {
Expand All @@ -66,6 +64,45 @@ private String joinTableClause(final SearchingTags searchingTags) {
.collect(Collectors.joining());
}

private String whereCondition(final Long id, final LocalDateTime createdAt, final String title,
final SearchingTags searchingTags) {
if (!hasCondition(id, createdAt, title, searchingTags)) {
return "";
}

final String cursorClause = filtersInCursorClause(id, createdAt);
final String titleClause = filtersTitleClause(title);
final String filtersInQueryClause = filtersInQueryClause(searchingTags);
final String combinedClause = combineClause(cursorClause, titleClause);

if (combinedClause.isBlank()) {
return "WHERE " + filtersInQueryClause.replaceFirst("AND ", "");
}

return "WHERE " + combineClause(cursorClause, titleClause) + filtersInQueryClause;
}

private boolean hasCondition(final Long id, final LocalDateTime createdAt,
final String title, final SearchingTags searchingTags) {
return id != null || createdAt != null || !title.isBlank() || !searchingTags.isEmpty();
}


private String filtersInCursorClause(final Long id, final LocalDateTime createdAt) {
if (id != null && createdAt != null) {
return "(study.created_at < :createdAt OR (study.created_at = :createdAt AND id < :id)) ";
}

return "";
}

private String filtersTitleClause(final String title) {
if (title.isBlank()) {
return "";
}
return "UPPER(study.title) LIKE UPPER(:title) ESCAPE '\' ";
}

private String filtersInQueryClause(final SearchingTags searchingTags) {
String sql = "AND {}_tag.id IN (:{}) ";

Expand All @@ -75,27 +112,47 @@ private String filtersInQueryClause(final SearchingTags searchingTags) {
.collect(Collectors.joining());
}

private Map<String, Object> params(final String title, final SearchingTags searchingTags,
final Pageable pageable) {
private String combineClause(String... clauses) {
final List<String> notBlankClauses = Arrays.stream(clauses)
.filter(it -> !it.isBlank())
.collect(Collectors.toList());

if (notBlankClauses.isEmpty()) {
return "";
}

StringBuilder stringBuilder = new StringBuilder(notBlankClauses.get(0));
for (int i = 1; i < notBlankClauses.size(); i++) {
stringBuilder.append("AND " + notBlankClauses.get(i));
}

return stringBuilder.toString();
}

private Map<String, Object> params(final String title, final SearchingTags searchingTags, final Long id,
final LocalDateTime createdAt,
final int size) {
final Map<String, Object> tagIds = Stream.of(CategoryName.values())
.collect(Collectors.toMap(name -> name.name().toLowerCase(), searchingTags::getTagIdsBy));

Map<String, Object> param = new HashMap<>();
param.put("title", "%" + title + "%");
param.put("limit", pageable.getPageSize() + 1);
param.put("offset", pageable.getOffset());
param.put("id", id);
param.put("createdAt", createdAt);
param.put("limit", size + 1);
param.putAll(tagIds);
return param;
}

private List<StudySummaryData> getCurrentPageStudies(final List<StudySummaryData> studies, final Pageable pageable) {
if (hasNext(studies, pageable)) {
private List<StudySummaryData> getCurrentPageStudies(final List<StudySummaryData> studies,
final int size) {
if (hasNext(studies, size)) {
return studies.subList(0, studies.size() - 1);
}
return studies;
}

private boolean hasNext(final List<StudySummaryData> studies, final Pageable pageable) {
return studies.size() > pageable.getPageSize();
private boolean hasNext(final List<StudySummaryData> studies, final int size) {
return studies.size() > size;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.woowacourse.moamoa.study.query.data;

import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -14,4 +15,5 @@ public class StudySummaryData {
private String excerpt;
private String thumbnail;
private String recruitmentStatus;
private LocalDateTime createdDate;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.woowacourse.moamoa.study.service;

import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;

import com.woowacourse.moamoa.member.query.MemberDao;
import com.woowacourse.moamoa.member.query.data.ParticipatingMemberData;
import com.woowacourse.moamoa.study.query.SearchingTags;
Expand All @@ -13,10 +15,10 @@
import com.woowacourse.moamoa.tag.query.TagDao;
import com.woowacourse.moamoa.tag.query.response.TagData;
import com.woowacourse.moamoa.tag.query.response.TagSummaryData;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -42,15 +44,18 @@ public SearchingStudyService(
this.tagDao = tagDao;
}

public StudiesResponse getStudies(final String title, final SearchingTags searchingTags, final Pageable pageable) {
final Slice<StudySummaryData> studyData = studySummaryDao.searchBy(title.trim(), searchingTags, pageable);
public StudiesResponse getStudies(final String title, final SearchingTags searchingTags, final Long id,
final String createdDate, final int size) {
final LocalDateTime lastCreatedDate = getCreatedDate(createdDate);
final Slice<StudySummaryData> studyData = studySummaryDao.searchBy(
title.trim(), searchingTags, id, lastCreatedDate, size);

final List<Long> studyIds = studyData.getContent().stream()
.map(StudySummaryData::getId)
.collect(Collectors.toList());
final Map<Long, List<TagSummaryData>> studyTags = tagDao.findTagsByStudyIds(studyIds);

return new StudiesResponse(studyData.getContent(), studyTags, studyData.hasNext());
return StudiesResponse.of(studyData.getContent(), studyTags, studyData.hasNext());
}

public StudyDetailResponse getStudyDetails(final Long studyId) {
Expand All @@ -61,4 +66,11 @@ public StudyDetailResponse getStudyDetails(final Long studyId) {
final List<TagData> attachedTags = tagDao.findTagsByStudyId(studyId);
return new StudyDetailResponse(content, participants, attachedTags);
}

private LocalDateTime getCreatedDate(final String createdDate) {
if (createdDate.isBlank()) {
return null;
}
return LocalDateTime.parse(createdDate, ISO_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
package com.woowacourse.moamoa.study.service.response;

import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;

import com.woowacourse.moamoa.study.query.data.StudySummaryData;
import com.woowacourse.moamoa.tag.query.response.TagSummaryData;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StudiesResponse {

private List<StudyResponse> studies;
private boolean hasNext;
private Long id;
private String createdDate;

public StudiesResponse(
final List<StudySummaryData> studySummaryData,
final Map<Long, List<TagSummaryData>> studyTags,
final boolean hasNext
) {

this.studies = getStudyResponses(studySummaryData, studyTags);
private StudiesResponse(final List<StudyResponse> studies, final boolean hasNext, final Long id,
final String createdDate) {
this.studies = studies;
this.hasNext = hasNext;
this.id = id;
this.createdDate = createdDate;
}

public static StudiesResponse of(final List<StudySummaryData> studySummaryData,
final Map<Long, List<TagSummaryData>> studyTags, final boolean hasNext) {
final List<StudyResponse> studies = getStudyResponses(studySummaryData, studyTags);
if (studies.isEmpty()) {
return new StudiesResponse(studies, hasNext, null, "");
}

final StudySummaryData lastStudy = getLastStudyResponse(studySummaryData);
return new StudiesResponse(studies, hasNext, lastStudy.getId(), lastStudy.getCreatedDate().format(ISO_DATE_TIME));
}

private List<StudyResponse> getStudyResponses(
private static List<StudyResponse> getStudyResponses(
final List<StudySummaryData> studiesSummaryData,
final Map<Long, List<TagSummaryData>> studyTags
) {
return studiesSummaryData.stream()
.map(studySummaryData -> new StudyResponse(studySummaryData, studyTags.get(studySummaryData.getId())))
.collect(Collectors.toList());
}

private static StudySummaryData getLastStudyResponse(final List<StudySummaryData> studies) {
return studies.get(studies.size() - 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.woowacourse.moamoa.MoamoaApplication;
import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse;
import com.woowacourse.moamoa.auth.service.request.AccessTokenRequest;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
Expand All @@ -34,7 +33,6 @@
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

@SpringBootTest(
webEnvironment = WebEnvironment.RANDOM_PORT,
Expand All @@ -47,17 +45,14 @@ public class AcceptanceTest {
protected RequestSpecification spec;

@RegisterExtension
final RestDocumentationExtension restDocumentation = new RestDocumentationExtension (OUTPUT_DIRECTORY);
final RestDocumentationExtension restDocumentation = new RestDocumentationExtension(OUTPUT_DIRECTORY);

@LocalServerPort
protected int port;

@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private RestTemplate restTemplate;

@Autowired
public SlackAlarmMockServer slackAlarmMockServer;

Expand Down
Loading