【问题标题】:Why is hibernate creating null foreign keys?为什么休眠创建空外键?
【发布时间】:2019-10-16 11:13:22
【问题描述】:

我正在尝试创建一个包含两个实体的 Spring Boot 应用程序:Question 和 QuestionChoices。我正在使用双向单对多关系。当我尝试创建一个 Question 实体以及 QuestionChoices 列表时,QuestionChoice 中的外键为空。

这是我的 QuestionChoice 实体:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QuestionChoice {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String choice;

    @ManyToOne
    @JoinColumn(name = "question_id")
    private Question question;

    public QuestionChoice(String choice, Question question) {
        this.choice = choice;
        this.question = question;
    }

    public QuestionChoice(String choice) {
        this.choice = choice;
    }

}

这是我的问题实体:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Question {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int question_id;
    private String questionName;
    private String questionText;

    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
    private List<QuestionChoice> questionChoices;

    public Question(String questionName, String questionText, List<QuestionChoice> questionChoices) {
        this.questionName = questionName;
        this.questionText = questionText;
        this.questionChoices = questionChoices;
        this.questionChoices.forEach(x -> x.setQuestion(this));
    }
}

我有一个 QuestionRepository 和 QuestionChoiceRepository:

@Repository
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}

@Repository
public interface QuestionChoiceRepository extends JpaRepository<QuestionChoice, Integer> {
}

这是我的控制器:

@RestController
public class Controller {

    QuestionRepository questionRepository;
    QuestionChoiceRepository questionChoiceRepository;

    public Controller(QuestionRepository questionRepository,
                      QuestionChoiceRepository questionChoiceRepository) {
        this.questionRepository = questionRepository;
        this.questionChoiceRepository = questionChoiceRepository;
    }

    @PostMapping("/question")
    public Question createQuestion(@RequestBody Question question) {
        return questionRepository.save(question);
    }

    @GetMapping("/question")
    public List<Question> getQuestions() {
        return questionRepository.findAll();
    }

}

这是我的 POST 请求:

POST http://localhost:8080/question
Content-Type: application/json

{
  "questionName": "gender",
  "questionText": "What is your gender?",
  "questionChoices": ["male", "female"]
}

这是来自 POST 的回复:

{
  "id": 1,
  "questionName": "gender",
  "questionText": "What is your gender?",
  "questionChoices": [
    {
      "id": 1,
      "choice": "male",
      "question": null
    },
    {
      "id": 2,
      "choice": "female",
      "question": null
    }
  ]
}

这是来自 GET 请求的响应:

GET http://localhost:8080/question

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 16 Oct 2019 11:10:51 GMT

[
  {
    "id": 1,
    "questionName": "gender",
    "questionText": "What is your gender?",
    "questionChoices": []
  }
]

因此,不仅 QuestionChoices 的外键为空,而且问题实体中的问题选择列表也为空。

知道我做错了什么吗?


更新

我在这里找到了解决此问题的好方法:Infinite Recursion with Jackson JSON and Hibernate JPA issue。问题在于杰克逊,而不是 Hibernate。只需向实体中的引用对象添加一个额外的注释,一切都很好!

【问题讨论】:

    标签: java hibernate spring-boot jpa


    【解决方案1】:

    您将在 JSON 正文中为您的 questionChoices 发送一个字符串数组。您的 JSON 映射器需要从此字符串数组中填充 List&lt;Question&gt;。所以它需要将每个String 转换成一个QuestionChoice 对象。据推测,它是通过调用将String 作为参数的QuestionChoice 构造函数来实现的。

    所以你保存了一个Question,它有一个QuestionChoices,它们都有一个空的question属性。所以你告诉 JPA 所有 QuestionChoices 都没有任何问题(因为它是空的)。因此,JPA 会保存您告诉它保存的内容:QuestionChoices without any parent question。

    您需要正确初始化QuestionChoicequestion 属性。

    【讨论】:

    • 我认为QuestionChoicequestion 属性在Question 构造函数中使用this.questionChoices.forEach(x -&gt; x.setQuestion(this)); 进行了初始化。我需要另一种方式来初始化它吗?
    • 我不知道您使用的是哪个 JSON 映射器,但我怀疑它使用构造函数来填充您的对象。它使用无参数 cnstructor,然后直接初始化字段,或者调用 setter。如果您希望它使用构造函数,则需要阅读其文档并将其配置为执行此操作(如果可能)。无论您选择哪种方式,都需要这样做。
    • 我正在使用默认的 JSON 映射器进行 Spring Boot,Jackson。看来我的 Question 构造函数没有被使用。当我尝试设置QuestionChoice 对象的question 字段时,我得到一个无限递归和stackoverflow。你知道如何防止这种情况发生吗?
    【解决方案2】:

    反序列化器将始终使用默认构造函数来构造对象。您的自定义构造函数对反序列化没有影响。

    你可以做的是:

    1 - 保证您的服务/控制器层中的关联

    @PostMapping("/question")
    public Question createQuestion(@RequestBody Question question) {
        question.getQuestionChoices().forEach(choice -> choice.setQuestion(question));
        return questionRepository.save(question);
    }
    

    或 2 - 在你的 setter 方法中保证关联:

    public class Question {
    
        // omitted for brevity
    
        @OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
        private List<QuestionChoice> questionChoices;
    
        public void setQuestionChoices(List<QuestionChoice> questionChoices) {
            if (questionChoices != null) {
                questionChoices.forEach(choice -> choice.setQuestion(this));
            }
            this.questionChoices = questionChoices;
        }
    }
    

    更新

    为防止无限递归,只需从“questionChoice”中删除“question”属性以进行演示。

    我能想到两个选择:

    1 - 在questionChoice 内部将question 设置为空

    @PostMapping("/question")
    public Question createQuestion(@RequestBody Question question) {
        Question savedQuestion = questionRepository.save(question);
        savedQuestion.getQuestionChoices().forEach(choice -> choice.setQuestion(null));
    
        return savedQuestion;
    }
    
    @GetMapping("/question")
    public List<Question> getQuestions() {
        List<Question> questions questionRepository.findAll();
        questions.forEach(question -> {
            question.getQuestionChoices.forEach(choice -> choice.setQuestion(null));
        });
    
        return questions;
    }
    

    这会将您的问题选择和外键保存到数据库中,但在发送响应时会将questionChoices.question 序列化为空,以防止无限递归。

    2 - 使用 DTO。

    您创建一个 DTO 以将它们序列化为响应对象,以准确返回您想要的内容。

    QuestionDTO.java

    public class QuestionDTO {
    
        private int question_id;
        private String questionName;
        private String questionText;
    
        // notice that here you're using composition of DTOs (QuestionChoiceDTO instead of QuestionChoice)
        private List<QuestionChoiceDTO> questionChoices;
    
        // constructors..
    
        // getters and setters..
    }
    

    QuestionChoiceDTO.java

    public class QuestionChoiceDTO {
    
        private int id;
        private String choice;
    
        // notice that you don't need to create the Question object here
    
        // constructors..
    
        // getters and setters..
    
    }
    

    然后在你的控制器中:

    @PostMapping("/question")
    public QuestionDTO createQuestion(@RequestBody Question question) {
        Question savedQuestion = questionRepository.save(question);
    
        List<QuestionChoiceDTO> questionChoices = new ArrayList<>();
        savedQuestion.getQuestionChoices().forEach(choice -> {
            questionChoices.add(new QuestionChoiceDTO(choice.getId(), choice.getChoice()));
        });
    
        QuestionDTO response = new QuestionDTO(savedQuestion.getQuestion_id(), savedQuestion.getQuestionName(), savedQuestion.getQuestionText(), questionChoices);
    
        return response;
    }
    
    @GetMapping("/question")
    public List<QuestionDTO> getQuestions() {
        List<Question> questions = questionRepository.findAll();
        List<QuestionDTO> response = new ArrayList<>();
    
        questions.forEach(question -> {
            List<QuestionChoicesDTO> questionChoices = new ArrayList<>();
            question.getQuestionChoices().forEach(choice -> questionChoices.add(new QuestionChoiceDTO(choice.getId(), choice.getChoice()));
    
            responses.add(new QuestionDTO(savedQuestion.getQuestion_id(), savedQuestion.getQuestionName(), savedQuestion.getQuestionText(), questionChoices));
        });
    }
    

    我总是更喜欢后者,因为对于大型项目,恕我直言,使用 DTO 可以成为组织代码和简洁地使用请求/响应对象而不使用域对象的强大工具。

    【讨论】:

    • 我认为这是在正确的轨道上。但是,当我尝试这两种方法时,我得到一个无限递归循环和 stackoverflow。这是来自 POST 请求的错误消息:"error": "OK", "message": "Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException 然后它就一直这样下去。
    • 你从哪里得到无限循环?在POSTGET 方法上?
    • 最初在 POST 上,但随后也在 GET 上。
    • 这是意料之中的。您输入了无限递归,因为当您发送响应时,您需要通过从 questionChoices 中删除“问题”属性来防止这种情况。如果您不这样做,则序列化程序会继续在另一个内部序列化。也许这是另一个问题的主题
    【解决方案3】:

    请求后您不使用构造函数 public Question(...) 。您应该制定一种方法将选择与问题联系起来

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多