From a953335ef595611307320d5830dd808247cb92bd Mon Sep 17 00:00:00 2001
From: lion2 <10328036@stud.op.edu.ua>
Date: Thu, 31 Jul 2025 13:18:05 +0200
Subject: [PATCH 1/2] feat(question): add DTOs, Redis cache, and new endpoints
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Added DTOs for Question and Answer entities
- Removed @JsonIgnore annotation on Question and replaced with DTO usage
- Implemented endpoints:
- GET /questions/random – get random question
- GET /questions/cached – get cached question from Redis
- GET /questions/search?keyword= – get questions by title keyword
- DELETE /questions?keyword= – delete questions by title keyword
- GET /questions/{id}/answers – get answers by question ID
---
pom.xml | 26 +++++++
.../example/postgresdemo/DTO/DtoAnswer.java | 26 +++++++
.../example/postgresdemo/DTO/DtoQuestion.java | 16 +++++
.../postgresdemo/Service/RedisConfig.java | 18 +++++
.../postgresdemo/Service/ServiceQuestion.java | 71 +++++++++++++++++++
.../controller/AnswerController.java | 16 ++++-
.../controller/QuestionController.java | 48 +++++++++++++
.../example/postgresdemo/model/Answer.java | 30 ++------
.../example/postgresdemo/model/Question.java | 36 +++-------
.../repository/AnswerRepository.java | 3 +
.../repository/QuestionRepository.java | 5 ++
src/main/resources/application.properties | 7 +-
12 files changed, 248 insertions(+), 54 deletions(-)
create mode 100644 src/main/java/com/example/postgresdemo/DTO/DtoAnswer.java
create mode 100644 src/main/java/com/example/postgresdemo/DTO/DtoQuestion.java
create mode 100644 src/main/java/com/example/postgresdemo/Service/RedisConfig.java
create mode 100644 src/main/java/com/example/postgresdemo/Service/ServiceQuestion.java
diff --git a/pom.xml b/pom.xml
index 2c23e76..571fcfd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,24 @@
spring-boot-starter-test
test
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
+
+ org.apache.commons
+ commons-pool2
+ 2.12.0
+
+
+
@@ -55,6 +73,14 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 13
+ 13
+
+
diff --git a/src/main/java/com/example/postgresdemo/DTO/DtoAnswer.java b/src/main/java/com/example/postgresdemo/DTO/DtoAnswer.java
new file mode 100644
index 0000000..19a77ea
--- /dev/null
+++ b/src/main/java/com/example/postgresdemo/DTO/DtoAnswer.java
@@ -0,0 +1,26 @@
+package com.example.postgresdemo.DTO;
+
+import com.example.postgresdemo.model.Question;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+
+import javax.persistence.Column;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DtoAnswer {
+ private String text;
+ private String title;
+ private String description;
+
+}
diff --git a/src/main/java/com/example/postgresdemo/DTO/DtoQuestion.java b/src/main/java/com/example/postgresdemo/DTO/DtoQuestion.java
new file mode 100644
index 0000000..49268cb
--- /dev/null
+++ b/src/main/java/com/example/postgresdemo/DTO/DtoQuestion.java
@@ -0,0 +1,16 @@
+package com.example.postgresdemo.DTO;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DtoQuestion {
+ private String title;
+ private String description;
+}
diff --git a/src/main/java/com/example/postgresdemo/Service/RedisConfig.java b/src/main/java/com/example/postgresdemo/Service/RedisConfig.java
new file mode 100644
index 0000000..8e1d4e6
--- /dev/null
+++ b/src/main/java/com/example/postgresdemo/Service/RedisConfig.java
@@ -0,0 +1,18 @@
+package com.example.postgresdemo.Service;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public JedisPool jedisPool() {
+ JedisPoolConfig config = new JedisPoolConfig();
+ config.setMaxTotal(8);
+ config.setJmxEnabled(false); // <-- Это отключает регистрацию MBean
+ return new JedisPool(config, "localhost", 6379);
+ }
+}
diff --git a/src/main/java/com/example/postgresdemo/Service/ServiceQuestion.java b/src/main/java/com/example/postgresdemo/Service/ServiceQuestion.java
new file mode 100644
index 0000000..3a69c49
--- /dev/null
+++ b/src/main/java/com/example/postgresdemo/Service/ServiceQuestion.java
@@ -0,0 +1,71 @@
+package com.example.postgresdemo.Service;
+
+import com.example.postgresdemo.DTO.DtoQuestion;
+import com.example.postgresdemo.model.Question;
+import com.example.postgresdemo.repository.QuestionRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+
+import java.util.Random;
+
+@Service
+public class ServiceQuestion {
+
+ private final QuestionRepository questionRepository;
+
+ @Autowired
+ private JedisPool jedisPool;
+ ObjectMapper mapper = new ObjectMapper();
+
+ private static final int TTL = 3600;
+
+ @Autowired
+ public ServiceQuestion(QuestionRepository questionRepository) {
+ this.questionRepository = questionRepository;
+ }
+
+ public DtoQuestion randomQuestion() {
+ long count = questionRepository.count();
+ if (count == 0){
+ return null;
+ }
+ long min = 1200;
+ long max = 1251;
+
+ long random = min + (long) (Math.random() * ((max - min) + 1));
+ return getCachedQuestion(random);
+ }
+
+ public DtoQuestion getQuestion(Long id) {
+ return questionRepository.findById(id).
+ map(qst -> new DtoQuestion(qst.getTitle(),
+ qst.getDescription())).orElse(null);
+ }
+
+ public DtoQuestion getCachedQuestion(Long id) {
+ try(Jedis jedis = jedisPool.getResource()) {
+
+ String key = String.format("article:%d",id);
+ String raw = jedis.get(key);
+ if(raw != null) {
+ return mapper.readValue(raw,DtoQuestion.class);
+ }
+ var article = getQuestion(id);
+ if(article == null) {
+ return null;
+ }
+
+ jedis.setex(key,TTL , mapper.writeValueAsString(article));
+ return article;
+ } catch (JsonMappingException e) {
+ throw new RuntimeException(e);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/example/postgresdemo/controller/AnswerController.java b/src/main/java/com/example/postgresdemo/controller/AnswerController.java
index 7cfaa47..40e5fe0 100644
--- a/src/main/java/com/example/postgresdemo/controller/AnswerController.java
+++ b/src/main/java/com/example/postgresdemo/controller/AnswerController.java
@@ -1,5 +1,6 @@
package com.example.postgresdemo.controller;
+import com.example.postgresdemo.DTO.DtoAnswer;
import com.example.postgresdemo.exception.ResourceNotFoundException;
import com.example.postgresdemo.model.Answer;
import com.example.postgresdemo.repository.AnswerRepository;
@@ -8,7 +9,9 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
+import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
@RestController
public class AnswerController {
@@ -20,8 +23,15 @@ public class AnswerController {
private QuestionRepository questionRepository;
@GetMapping("/questions/{questionId}/answers")
- public List getAnswersByQuestionId(@PathVariable Long questionId) {
- return answerRepository.findByQuestionId(questionId);
+ public List getAnswersByQuestionId(@PathVariable Long questionId) {
+ List answer = answerRepository.findByQuestionId(questionId);
+
+ List dtoAnswers = answer.stream().
+ map(ans -> new DtoAnswer(ans.getText(),
+ ans.getQuestion().getDescription(),
+ ans.getQuestion().getDescription())).
+ collect(Collectors.toList());
+ return dtoAnswers;
}
@PostMapping("/questions/{questionId}/answers")
@@ -63,4 +73,6 @@ public ResponseEntity> deleteAnswer(@PathVariable Long questionId,
}).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId));
}
+
+
}
diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java
index c231819..e887373 100644
--- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java
+++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java
@@ -1,7 +1,11 @@
package com.example.postgresdemo.controller;
+import com.example.postgresdemo.DTO.DtoQuestion;
+import com.example.postgresdemo.Service.ServiceQuestion;
import com.example.postgresdemo.exception.ResourceNotFoundException;
+import com.example.postgresdemo.model.Answer;
import com.example.postgresdemo.model.Question;
+import com.example.postgresdemo.repository.AnswerRepository;
import com.example.postgresdemo.repository.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -9,6 +13,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
+import java.util.List;
@RestController
public class QuestionController {
@@ -16,6 +21,12 @@ public class QuestionController {
@Autowired
private QuestionRepository questionRepository;
+ @Autowired
+ private AnswerRepository answerRepository;
+
+ @Autowired
+ private ServiceQuestion serviceQuestion;
+
@GetMapping("/questions")
public Page getQuestions(Pageable pageable) {
return questionRepository.findAll(pageable);
@@ -47,4 +58,41 @@ public ResponseEntity> deleteQuestion(@PathVariable Long questionId) {
return ResponseEntity.ok().build();
}).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId));
}
+
+ @GetMapping("/getAllAnswer/{questionId}")
+ public List getAllAnswersOnQuestion(
+ @PathVariable Long questionId) {
+ return answerRepository.findAllByQuestionId(questionId);
+ }
+
+ @GetMapping("/getQuestionByTitle")
+ public List getQuestionByTitle(
+ @RequestParam String keyword) {
+ return questionRepository.findByTitleContainingIgnoreCase(keyword);
+ }
+
+ @DeleteMapping("/deleteQuestionByTitle")
+ public ResponseEntity> deleteQuestionByTitle(
+ @RequestParam String keyword) {
+ return questionRepository.findByTitleContainingIgnoreCase(keyword)
+ .stream()
+ .peek(questionRepository::delete)
+ .findAny()
+ .map(question -> ResponseEntity.ok().build())
+ .orElseThrow(() ->
+ new ResourceNotFoundException
+ ("Question not found with key Word " + keyword));
+ }
+
+ @GetMapping("/randomQuestion")
+ public DtoQuestion getRandomQuestion() {
+ return serviceQuestion.randomQuestion();
+ }
+
+
+ @GetMapping("/questionCached/{id}")
+ public DtoQuestion getCachedQuestion(@PathVariable Long id) {
+ return serviceQuestion.getCachedQuestion(id);
+ }
+
}
diff --git a/src/main/java/com/example/postgresdemo/model/Answer.java b/src/main/java/com/example/postgresdemo/model/Answer.java
index b5e48d0..a876d9a 100644
--- a/src/main/java/com/example/postgresdemo/model/Answer.java
+++ b/src/main/java/com/example/postgresdemo/model/Answer.java
@@ -1,13 +1,17 @@
package com.example.postgresdemo.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
@Entity
@Table(name = "answers")
+@Data
public class Answer extends AuditModel {
@Id
@GeneratedValue(generator = "answer_generator")
@@ -19,35 +23,13 @@ public class Answer extends AuditModel {
private Long id;
@Column(columnDefinition = "text")
+ @NotBlank(message = "Description is mandatory")
+ @Size(min = 10, max = 500, message = "Description must be between 10 and 500 characters")
private String text;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "question_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
- @JsonIgnore
private Question question;
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getText() {
- return text;
- }
-
- public void setText(String text) {
- this.text = text;
- }
-
- public Question getQuestion() {
- return question;
- }
-
- public void setQuestion(Question question) {
- this.question = question;
- }
}
diff --git a/src/main/java/com/example/postgresdemo/model/Question.java b/src/main/java/com/example/postgresdemo/model/Question.java
index d16a459..6578902 100644
--- a/src/main/java/com/example/postgresdemo/model/Question.java
+++ b/src/main/java/com/example/postgresdemo/model/Question.java
@@ -1,11 +1,15 @@
package com.example.postgresdemo.model;
+
+import lombok.Data;
+
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Entity
@Table(name = "questions")
+@Data
public class Question extends AuditModel {
@Id
@GeneratedValue(generator = "question_generator")
@@ -16,34 +20,14 @@ public class Question extends AuditModel {
)
private Long id;
- @NotBlank
- @Size(min = 3, max = 100)
- private String title;
-
- @Column(columnDefinition = "text")
- private String description;
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getTitle() {
- return title;
- }
+ @NotBlank(message = "Title is mandatory")
+ @Size(min = 5, max = 100, message = "Title must be between 5 and 100 characters")
+ private String title;
- public void setTitle(String title) {
- this.title = title;
- }
- public String getDescription() {
- return description;
- }
+ @NotBlank(message = "Description is mandatory")
+ @Size(min = 10, max = 500, message = "Description must be between 10 and 500 characters")
+ private String description;
- public void setDescription(String description) {
- this.description = description;
- }
}
diff --git a/src/main/java/com/example/postgresdemo/repository/AnswerRepository.java b/src/main/java/com/example/postgresdemo/repository/AnswerRepository.java
index 761f91f..0e25b6f 100644
--- a/src/main/java/com/example/postgresdemo/repository/AnswerRepository.java
+++ b/src/main/java/com/example/postgresdemo/repository/AnswerRepository.java
@@ -1,6 +1,8 @@
package com.example.postgresdemo.repository;
import com.example.postgresdemo.model.Answer;
+import com.example.postgresdemo.model.Question;
+import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -8,4 +10,5 @@
@Repository
public interface AnswerRepository extends JpaRepository {
List findByQuestionId(Long questionId);
+ List findAllByQuestionId(Long questionId);
}
diff --git a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java
index 290373d..5ac826f 100644
--- a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java
+++ b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java
@@ -1,9 +1,14 @@
package com.example.postgresdemo.repository;
+import com.example.postgresdemo.model.Answer;
import com.example.postgresdemo.model.Question;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Repository;
+import java.util.List;
+
@Repository
public interface QuestionRepository extends JpaRepository {
+ List findByTitleContainingIgnoreCase(String title);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 35b376a..6a09cd8 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,10 +1,13 @@
## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
-spring.datasource.url=jdbc:postgresql://localhost:5432/postgres_demo
+spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username= postgres
-spring.datasource.password=
+spring.datasource.password=200504
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
+
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
From b93c8084ff60c73af25e103f16c18e64e04edc00 Mon Sep 17 00:00:00 2001
From: lion2 <10328036@stud.op.edu.ua>
Date: Sat, 2 Aug 2025 20:43:57 +0200
Subject: [PATCH 2/2] new idea
---
.../com/example/postgresdemo/Service/RedisConfig.java | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/example/postgresdemo/Service/RedisConfig.java b/src/main/java/com/example/postgresdemo/Service/RedisConfig.java
index 8e1d4e6..be46afa 100644
--- a/src/main/java/com/example/postgresdemo/Service/RedisConfig.java
+++ b/src/main/java/com/example/postgresdemo/Service/RedisConfig.java
@@ -12,7 +12,11 @@ public class RedisConfig {
public JedisPool jedisPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(8);
- config.setJmxEnabled(false); // <-- Это отключает регистрацию MBean
- return new JedisPool(config, "localhost", 6379);
+ config.setJmxEnabled(false);
+
+ String redisHost = System.getenv("REDIS_HOST");
+ int redisPort = Integer.parseInt(System.getenv("REDIS_PORT"));
+
+ return new JedisPool(config, redisHost, redisPort);
}
}