diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/models/question/topic/QuestionTopic.java b/src/main/java/org/patinanetwork/codebloom/common/db/models/question/topic/QuestionTopic.java index dddc6c836..1ca48d13b 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/models/question/topic/QuestionTopic.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/models/question/topic/QuestionTopic.java @@ -1,6 +1,7 @@ package org.patinanetwork.codebloom.common.db.models.question.topic; import java.time.LocalDateTime; +import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -22,10 +23,10 @@ public class QuestionTopic { private String id; @NullColumn - private String questionId; + private Optional questionId; @NullColumn - private String questionBankId; + private Optional questionBankId; @NotNullColumn private String topicSlug; @@ -35,4 +36,26 @@ public class QuestionTopic { @NotNullColumn private LocalDateTime createdAt; + + public static class QuestionTopicBuilder { + public QuestionTopicBuilder questionId(String questionId) { + this.questionId = Optional.ofNullable(questionId); + return this; + } + + public QuestionTopicBuilder questionBankId(String questionBankId) { + this.questionBankId = Optional.ofNullable(questionBankId); + return this; + } + + public QuestionTopic build() { + if (this.questionId == null) { + this.questionId = Optional.empty(); + } + if (this.questionBankId == null) { + this.questionBankId = Optional.empty(); + } + return new QuestionTopic(id, questionId, questionBankId, topicSlug, topic, createdAt); + } + } } diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepository.java index 1692f16b5..d69fe9d05 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepository.java @@ -1,17 +1,19 @@ package org.patinanetwork.codebloom.common.db.repos.question.topic; import java.util.List; +import java.util.Optional; import org.patinanetwork.codebloom.common.db.models.question.topic.LeetcodeTopicEnum; import org.patinanetwork.codebloom.common.db.models.question.topic.QuestionTopic; public interface QuestionTopicRepository { + List findQuestionTopicsByQuestionId(String questionId); List findQuestionTopicsByQuestionBankId(String questionBankId); - QuestionTopic findQuestionTopicById(String id); + Optional findQuestionTopicById(String id); - QuestionTopic findQuestionTopicByQuestionIdAndTopicEnum(String questionId, LeetcodeTopicEnum topicEnum); + Optional findQuestionTopicByQuestionIdAndTopicEnum(String questionId, LeetcodeTopicEnum topicEnum); /** * @note - The provided object's methods will be overridden with any returned data from the database. diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicSqlRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicSqlRepository.java index bec17707c..80a320051 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicSqlRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicSqlRepository.java @@ -5,6 +5,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import javax.sql.DataSource; import org.patinanetwork.codebloom.common.db.helper.NamedPreparedStatement; @@ -15,23 +16,30 @@ @Component public class QuestionTopicSqlRepository implements QuestionTopicRepository { - private final DataSource ds; + private static final String COL_ID = "id"; + private static final String COL_QUESTION_ID = "questionId"; + private static final String COL_QUESTION_BANK_ID = "questionBankId"; + private static final String COL_TOPIC_SLUG = "topicSlug"; + private static final String COL_TOPIC = "topic"; + private static final String COL_CREATED_AT = "createdAt"; - private QuestionTopic mapResultSetToQuestionTopic(final ResultSet resultSet) throws SQLException { - return QuestionTopic.builder() - .id(resultSet.getString("id")) - .createdAt(resultSet.getTimestamp("createdAt").toLocalDateTime()) - .questionId(resultSet.getString("questionId")) - .questionBankId(resultSet.getString("questionBankId")) - .topicSlug(resultSet.getString("topicSlug")) - .topic(LeetcodeTopicEnum.fromValue(resultSet.getString("topic"))) - .build(); - } + private final DataSource ds; public QuestionTopicSqlRepository(final DataSource ds) { this.ds = ds; } + private QuestionTopic mapResultSetToQuestionTopic(final ResultSet rs) throws SQLException { + return QuestionTopic.builder() + .id(rs.getString(COL_ID)) + .createdAt(rs.getTimestamp(COL_CREATED_AT).toLocalDateTime()) + .questionId(rs.getString(COL_QUESTION_ID)) + .questionBankId(rs.getString(COL_QUESTION_BANK_ID)) + .topicSlug(rs.getString(COL_TOPIC_SLUG)) + .topic(LeetcodeTopicEnum.fromValue(rs.getString(COL_TOPIC))) + .build(); + } + @Override public List findQuestionTopicsByQuestionId(final String questionId) { List result = new ArrayList<>(); @@ -48,11 +56,11 @@ public List findQuestionTopicsByQuestionId(final String questionI "QuestionTopic" qt WHERE qt."questionId" = :questionId - """; + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("questionId", UUID.fromString(questionId)); + stmt.setObject(COL_QUESTION_ID, UUID.fromString(questionId)); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { @@ -82,11 +90,11 @@ public List findQuestionTopicsByQuestionBankId(final String quest "QuestionTopic" qt WHERE qt."questionBankId" = :questionBankId - """; + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("questionBankId", UUID.fromString(questionBankId)); + stmt.setObject(COL_QUESTION_BANK_ID, UUID.fromString(questionBankId)); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { @@ -101,41 +109,39 @@ public List findQuestionTopicsByQuestionBankId(final String quest } @Override - public QuestionTopic findQuestionTopicById(final String id) { - try { - String sql = """ - SELECT - id, - "questionId", - "questionBankId", - "topicSlug", - "createdAt", - "topic" - FROM - "QuestionTopic" qt - WHERE - qt.id = :id + public Optional findQuestionTopicById(final String id) { + String sql = """ + SELECT + id, + "questionId", + "questionBankId", + "topicSlug", + "createdAt", + "topic" + FROM + "QuestionTopic" qt + WHERE + qt.id = :id """; - try (Connection conn = ds.getConnection(); - NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("id", UUID.fromString(id)); + try (Connection conn = ds.getConnection(); + NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { + stmt.setObject(COL_ID, UUID.fromString(id)); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - return mapResultSetToQuestionTopic(rs); - } + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(mapResultSetToQuestionTopic(rs)); } } - return null; + return Optional.empty(); } catch (Exception e) { throw new RuntimeException("Failed to get question topic by ID", e); } } @Override - public QuestionTopic findQuestionTopicByQuestionIdAndTopicEnum( + public Optional findQuestionTopicByQuestionIdAndTopicEnum( final String questionId, final LeetcodeTopicEnum topicEnum) { String sql = """ SELECT @@ -151,60 +157,50 @@ public QuestionTopic findQuestionTopicByQuestionIdAndTopicEnum( qt."questionId" = :questionId AND qt.topic = :topic - """; + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("questionId", UUID.fromString(questionId)); - stmt.setObject("topic", topicEnum.getLeetcodeEnum(), java.sql.Types.OTHER); + stmt.setObject(COL_QUESTION_ID, UUID.fromString(questionId)); + stmt.setObject(COL_TOPIC, topicEnum.getLeetcodeEnum(), java.sql.Types.OTHER); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { - return mapResultSetToQuestionTopic(rs); + return Optional.of(mapResultSetToQuestionTopic(rs)); } } - return null; + return Optional.empty(); } catch (Exception e) { - throw new RuntimeException("Failed to get question topic by ID", e); + throw new RuntimeException("Failed to get question topic by question ID and topic enum", e); } } @Override public void createQuestionTopic(final QuestionTopic questionTopic) { String sql = """ - INSERT INTO "QuestionTopic" - ("id", "questionId", "questionBankId", "topicSlug", "topic") - VALUES - (:id, :questionId, :questionBankId, :topicSlug, :topic) - RETURNING - "createdAt" - """; + INSERT INTO "QuestionTopic" ("id", "questionId", "questionBankId", "topicSlug", "topic") + VALUES (:id, :questionId, :questionBankId, :topicSlug, :topic) + RETURNING "createdAt" + """; questionTopic.setId(UUID.randomUUID().toString()); try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("id", UUID.fromString(questionTopic.getId())); - - if (questionTopic.getQuestionId() == null) { - stmt.setNull("questionId", java.sql.Types.NULL); - } else { - stmt.setObject("questionId", UUID.fromString(questionTopic.getQuestionId())); - } - - if (questionTopic.getQuestionBankId() == null) { - stmt.setNull("questionBankId", java.sql.Types.NULL); - } else { - stmt.setObject("questionBankId", UUID.fromString(questionTopic.getQuestionBankId())); - } - - stmt.setString("topicSlug", questionTopic.getTopicSlug()); - stmt.setObject("topic", questionTopic.getTopic().getLeetcodeEnum(), java.sql.Types.OTHER); + stmt.setObject(COL_ID, UUID.fromString(questionTopic.getId())); + stmt.setObject( + COL_QUESTION_ID, + questionTopic.getQuestionId().map(UUID::fromString).orElse(null)); + stmt.setObject( + COL_QUESTION_BANK_ID, + questionTopic.getQuestionBankId().map(UUID::fromString).orElse(null)); + stmt.setString(COL_TOPIC_SLUG, questionTopic.getTopicSlug()); + stmt.setObject(COL_TOPIC, questionTopic.getTopic().getLeetcodeEnum(), java.sql.Types.OTHER); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { - questionTopic.setCreatedAt(rs.getTimestamp("createdAt").toLocalDateTime()); + questionTopic.setCreatedAt(rs.getTimestamp(COL_CREATED_AT).toLocalDateTime()); } } } catch (Exception e) { @@ -215,35 +211,26 @@ public void createQuestionTopic(final QuestionTopic questionTopic) { @Override public boolean updateQuestionTopicById(final QuestionTopic questionTopic) { String sql = """ - UPDATE - "QuestionTopic" - SET - "questionId" = :questionId, - "questionBankId" = :questionBankId, - "topicSlug" = :topicSlug, - "topic" = :topic - WHERE - id = :id - """; + UPDATE "QuestionTopic" + SET + "questionId" = :questionId, + "questionBankId" = :questionBankId, + "topicSlug" = :topicSlug, + "topic" = :topic + WHERE id = :id + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("id", UUID.fromString(questionTopic.getId())); - - if (questionTopic.getQuestionId() == null) { - stmt.setNull("questionId", java.sql.Types.NULL); - } else { - stmt.setObject("questionId", UUID.fromString(questionTopic.getQuestionId())); - } - - if (questionTopic.getQuestionBankId() == null) { - stmt.setNull("questionBankId", java.sql.Types.NULL); - } else { - stmt.setObject("questionBankId", UUID.fromString(questionTopic.getQuestionBankId())); - } - - stmt.setString("topicSlug", questionTopic.getTopicSlug()); - stmt.setObject("topic", questionTopic.getTopic().getLeetcodeEnum(), java.sql.Types.OTHER); + stmt.setObject(COL_ID, UUID.fromString(questionTopic.getId())); + stmt.setObject( + COL_QUESTION_ID, + questionTopic.getQuestionId().map(UUID::fromString).orElse(null)); + stmt.setObject( + COL_QUESTION_BANK_ID, + questionTopic.getQuestionBankId().map(UUID::fromString).orElse(null)); + stmt.setString(COL_TOPIC_SLUG, questionTopic.getTopicSlug()); + stmt.setObject(COL_TOPIC, questionTopic.getTopic().getLeetcodeEnum(), java.sql.Types.OTHER); return stmt.executeUpdate() > 0; } catch (Exception e) { @@ -254,18 +241,14 @@ public boolean updateQuestionTopicById(final QuestionTopic questionTopic) { @Override public boolean deleteQuestionTopicById(final String id) { String sql = """ - DELETE FROM - "QuestionTopic" - WHERE - id = :id - """; + DELETE FROM "QuestionTopic" + WHERE id = :id + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { - stmt.setObject("id", UUID.fromString(id)); - int rowsAffected = stmt.executeUpdate(); - - return rowsAffected > 0; + stmt.setObject(COL_ID, UUID.fromString(id)); + return stmt.executeUpdate() > 0; } catch (SQLException e) { throw new RuntimeException("Failed to delete tag by tag ID", e); } diff --git a/src/main/java/org/patinanetwork/codebloom/common/dto/question/topic/QuestionTopicDto.java b/src/main/java/org/patinanetwork/codebloom/common/dto/question/topic/QuestionTopicDto.java index b62fdc4ef..c52195d70 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/dto/question/topic/QuestionTopicDto.java +++ b/src/main/java/org/patinanetwork/codebloom/common/dto/question/topic/QuestionTopicDto.java @@ -1,7 +1,9 @@ package org.patinanetwork.codebloom.common.dto.question.topic; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; +import java.util.Optional; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -15,13 +17,17 @@ @Jacksonized @ToString @EqualsAndHashCode +@JsonInclude(JsonInclude.Include.NON_EMPTY) public class QuestionTopicDto { @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String id; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - private String questionId; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Optional questionId; + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Optional questionBankId; @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String topicSlug; @@ -36,6 +42,7 @@ public static QuestionTopicDto fromQuestionTopic(final QuestionTopic questionTop return QuestionTopicDto.builder() .id(questionTopic.getId()) .questionId(questionTopic.getQuestionId()) + .questionBankId(questionTopic.getQuestionBankId()) .topicSlug(questionTopic.getTopicSlug()) .topic(questionTopic.getTopic()) .createdAt(questionTopic.getCreatedAt()) diff --git a/src/test/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepositoryTest.java b/src/test/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepositoryTest.java index 74eb58f78..4c0472a53 100644 --- a/src/test/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepositoryTest.java +++ b/src/test/java/org/patinanetwork/codebloom/common/db/repos/question/topic/QuestionTopicRepositoryTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.List; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -23,7 +24,7 @@ @Slf4j public class QuestionTopicRepositoryTest extends BaseRepositoryTest { - private QuestionTopicRepository questionTopicRepository; + private final QuestionTopicRepository questionTopicRepository; private final String mockQuestionId = "c9857a8a-9d0b-4d2e-b73c-3af2425bdca6"; private final String mockQuestionBankId = "165dd000-c310-11f0-8d3a-461b1b1abee8"; @@ -65,12 +66,13 @@ void cleanUp() { @Test @Order(1) void testFindQuestionTopicById() { - QuestionTopic possibleQuestionTopic = questionTopicRepository.findQuestionTopicById(testQuestionTopic.getId()); + Optional possibleQuestionTopic = + questionTopicRepository.findQuestionTopicById(testQuestionTopic.getId()); - assertNotNull(possibleQuestionTopic, "Retrieved question topic should not be null"); + assertTrue(possibleQuestionTopic.isPresent(), "Retrieved question topic should not be empty"); - if (!possibleQuestionTopic.equals(testQuestionTopic)) { - log.info("possibleQuestionTopic: {}", possibleQuestionTopic); + if (!possibleQuestionTopic.get().equals(testQuestionTopic)) { + log.info("possibleQuestionTopic: {}", possibleQuestionTopic.get()); log.info("testQuestionTopic: {}", testQuestionTopic); fail("testFindQuestionTopicById failed: possibleQuestionTopic does not equal to testQuestionTopic"); } @@ -79,13 +81,14 @@ void testFindQuestionTopicById() { @Test @Order(2) void testFindQuestionTopicByQuestionIdAndTopicEnum() { - QuestionTopic possibleQuestionTopic = questionTopicRepository.findQuestionTopicByQuestionIdAndTopicEnum( - mockQuestionId, LeetcodeTopicEnum.ARRAY); + Optional possibleQuestionTopic = + questionTopicRepository.findQuestionTopicByQuestionIdAndTopicEnum( + mockQuestionId, LeetcodeTopicEnum.ARRAY); - assertNotNull(possibleQuestionTopic, "Retrieved question topic should not be null"); + assertTrue(possibleQuestionTopic.isPresent(), "Retrieved question topic should not be empty"); - if (!possibleQuestionTopic.equals(testQuestionTopic)) { - log.info("possibleQuestionTopic: {}", possibleQuestionTopic); + if (!possibleQuestionTopic.get().equals(testQuestionTopic)) { + log.info("possibleQuestionTopic: {}", possibleQuestionTopic.get()); log.info("testQuestionTopic: {}", testQuestionTopic); fail( "testFindQuestionTopicByQuestionIdAndTopicEnum failed: possibleQuestionTopic does not equal to testQuestionTopic"); @@ -118,14 +121,15 @@ void testUpdateQuestion() { fail("testUpdateQuestion failed: Failed to update question"); } - QuestionTopic newQuestionTopic = questionTopicRepository.findQuestionTopicById(testQuestionTopic.getId()); + Optional newQuestionTopic = + questionTopicRepository.findQuestionTopicById(testQuestionTopic.getId()); - assertNotNull(newQuestionTopic, "Retrieved question topic should not be null"); + assertTrue(newQuestionTopic.isPresent(), "Retrieved question topic should not be empty"); - if (!newQuestionTopic.equals(testQuestionTopic)) { - log.info("newQuestionTopic: {}", newQuestionTopic); + if (!newQuestionTopic.get().equals(testQuestionTopic)) { + log.info("newQuestionTopic: {}", newQuestionTopic.get()); log.info("testQuestionTopic: {}", testQuestionTopic); - fail("testFindQuestionTopicById failed: newQuestionTopic does not equal to testQuestionTopic"); + fail("testUpdateQuestion failed: newQuestionTopic does not equal to testQuestionTopic"); } } @@ -140,7 +144,7 @@ void testFindQuestionTopicsByQuestionBankId() { if (!questionBankTopics.contains(testQuestionBankTopic)) { log.info("questionBankTopics: {}", questionBankTopics); log.info("testQuestionBankTopic: {}", testQuestionBankTopic); - fail("testFindQuestionTopicsByQuestionBankId failed: testQuestionBankTopic is not in questionTopics"); + fail("testFindQuestionTopicsByQuestionBankId failed: testQuestionBankTopic is not in questionBankTopics"); } } }