基于Spring JPA构建学习类答题游戏的技术实现咨询
嘿,我来帮你一步步搞定基于Spring JPA的答题游戏实现,先从你提到的实体关联错误入手,再解决多类型答案的问题,最后实现仓库存储和筛选功能~
一、先修复核心实体的关联错误
你原代码里的Question和Answer关联完全搞反了:一个Question对应多个Answer,所以Question这边应该用@OneToMany,而Answer用@ManyToOne关联回Question。另外,我也帮你补充了Topic、SubTopic、Quiz的完整实体实现,完全匹配你的规划:
修正后的Question实体
@Entity(name = "question") public class Question extends DateAudit { @Id @Column(name = "question_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "question_seq") @SequenceGenerator(name = "question_seq", allocationSize = 1) private Long id; @Column(name = "text") @NotBlank(message = "Question text can not be blank") private String text; @Column(name = "description") private String description; @Column(name = "difficulty") @Enumerated(EnumType.STRING) // 推荐用枚举管理难度,更规范 private Difficulty difficulty; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "created_by_user_id") private User userWhoCreated; // 假设你有User实体关联创建者 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "topic_id") private Topic topic; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "sub_topic_id") private SubTopic subTopic; @Column(name = "is_exam_question", nullable = false) private Boolean isExamQuestion; // 修复关联:双向绑定Answer的question字段,级联操作+孤儿移除确保数据一致性 @OneToMany(mappedBy = "question", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY, orphanRemoval = true) private Set<Answer> answers = new HashSet<>(); // 省略构造器、getter、setter } // 难度枚举示例 public enum Difficulty { EASY, MEDIUM, HARD }
修正后的Answer实体
@Entity(name = "answer") public class Answer extends DateAudit { @Id @Column(name = "answer_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "answer_seq") @SequenceGenerator(name = "answer_seq", allocationSize = 1) private Long id; @Column(name = "text") @NotBlank(message = "Answer text can not be blank") private String text; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "question_id", nullable = false) private Question question; @Column(name = "is_correct", nullable = false) private Boolean isCorrect; // 所有类型答案都需要标记是否正确 @Column(name = "answer_type") @Enumerated(EnumType.STRING) private AnswerType answerType; // 省略构造器、getter、setter } // 答案类型枚举 public enum AnswerType { TRUE_FALSE, YES_NO, SHORT_DESCRIPTION, MULTI_CHOICE }
补充Topic、SubTopic、Quiz实体
// Topic实体 @Entity(name = "topic") public class Topic { @Id @Column(name = "topic_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "topic_seq") @SequenceGenerator(name = "topic_seq", allocationSize = 1) private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @OneToMany(mappedBy = "topic", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY) private Set<Question> questions = new HashSet<>(); // 省略构造器、getter、setter } // SubTopic实体 @Entity(name = "sub_topic") public class SubTopic { @Id @Column(name = "sub_topic_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sub_topic_seq") @SequenceGenerator(name = "sub_topic_seq", allocationSize = 1) private Long id; @Column(name = "name", nullable = false) private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "topic_id") // 子主题关联父主题,支持层级结构 private Topic topic; @OneToMany(mappedBy = "subTopic", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY) private Set<Question> questions = new HashSet<>(); // 省略构造器、getter、setter } // Quiz实体(多对多关联Question) @Entity(name = "quiz") public class Quiz extends DateAudit { @Id @Column(name = "quiz_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "quiz_seq") @SequenceGenerator(name = "quiz_seq", allocationSize = 1) private Long id; @Column(name = "name", nullable = false) private String name; @Column(name = "description") private String description; @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "quiz_question", joinColumns = @JoinColumn(name = "quiz_id"), inverseJoinColumns = @JoinColumn(name = "question_id") ) private Set<Question> questions = new HashSet<>(); // 省略构造器、getter、setter }
二、多类型答案的实现方案
针对你提到的4种答案类型,有两种常用实现方式:
1. 枚举驱动(简单场景)
就是上面用的AnswerType枚举,所有答案存在同一张表,通过answer_type字段区分类型,适合各类型答案结构差异不大的场景(比如只需要文本和正确标记)。
2. JPA继承策略(复杂场景)
如果不同类型答案需要额外字段(比如多选题需要选项顺序、简答题需要字数限制),可以用单表继承(所有子类存在同一张表,用鉴别器字段区分):
// 父类Answer(抽象类) @Entity(name = "answer") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "answer_type", discriminatorType = DiscriminatorType.STRING) public abstract class Answer extends DateAudit { @Id @Column(name = "answer_id") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "answer_seq") @SequenceGenerator(name = "answer_seq", allocationSize = 1) private Long id; @Column(name = "text") @NotBlank(message = "Answer text can not be blank") private String text; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "question_id", nullable = false) private Question question; @Column(name = "is_correct", nullable = false) private Boolean isCorrect; // 省略getter、setter } // 判断题子类 @Entity @DiscriminatorValue("TRUE_FALSE") public class TrueFalseAnswer extends Answer { // 无额外字段 } // 多选题子类 @Entity @DiscriminatorValue("MULTI_CHOICE") public class MultiChoiceAnswer extends Answer { @Column(name = "option_order") private Integer optionOrder; // 标记选项顺序(如1=A、2=B) } // 简答题子类 @Entity @DiscriminatorValue("SHORT_DESCRIPTION") public class ShortDescriptionAnswer extends Answer { @Column(name = "max_length") private Integer maxLength; // 允许的最大回答长度 }
三、仓库层实现(问题存储与筛选)
用Spring Data JPA的Repository接口,轻松实现你需要的存储和筛选功能:
QuestionRepository
public interface QuestionRepository extends JpaRepository<Question, Long> { // 获取所有非考试用的学习问题 List<Question> findByIsExamQuestionFalse(); // 根据Topic和SubTopic筛选问题 List<Question> findByTopicAndSubTopic(Topic topic, SubTopic subTopic); // 根据Topic筛选问题 List<Question> findByTopic(Topic topic); // 根据难度筛选问题 List<Question> findByDifficulty(Difficulty difficulty); // 模糊搜索问题文本 List<Question> findByTextContainingIgnoreCase(String keyword); }
TopicRepository和SubTopicRepository
public interface TopicRepository extends JpaRepository<Topic, Long> { // 根据名称查询Topic(确保唯一) Optional<Topic> findByName(String name); } public interface SubTopicRepository extends JpaRepository<SubTopic, Long> { // 根据Topic和名称查询SubTopic Optional<SubTopic> findByNameAndTopic(String name, Topic topic); }
四、业务逻辑层核心功能示例
在Service层封装业务逻辑,比如筛选问题、创建问题:
@Service public class QuestionService { private final QuestionRepository questionRepository; private final TopicRepository topicRepository; private final SubTopicRepository subTopicRepository; // 构造器注入(推荐) public QuestionService(QuestionRepository questionRepository, TopicRepository topicRepository, SubTopicRepository subTopicRepository) { this.questionRepository = questionRepository; this.topicRepository = topicRepository; this.subTopicRepository = subTopicRepository; } // 获取所有学习用问题 public List<Question> getLearningQuestions() { return questionRepository.findByIsExamQuestionFalse(); } // 根据Topic和SubTopic名称筛选问题 public List<Question> filterQuestions(String topicName, String subTopicName) { Topic topic = topicRepository.findByName(topicName) .orElseThrow(() -> new IllegalArgumentException("Topic not found: " + topicName)); SubTopic subTopic = subTopicRepository.findByNameAndTopic(subTopicName, topic) .orElseThrow(() -> new IllegalArgumentException("SubTopic not found: " + subTopicName)); return questionRepository.findByTopicAndSubTopic(topic, subTopic); } // 创建包含答案的问题 public Question createQuestion(Question question) { // 确保每个答案都关联当前问题 question.getAnswers().forEach(answer -> answer.setQuestion(question)); return questionRepository.save(question); } }
这样就完全实现了你需要的核心功能:问题存储、基于Topic/SubTopic的筛选,以及多类型答案的支持。
内容的提问来源于stack exchange,提问作者StuartDTO
相关产品推荐
相关产品推荐

