You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:15:00