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); } }