Reputation: 317
I am trying to create a spring boot application with two entities: Question and QuestionChoices. I'm using a bidirectional onetomany relationship. When I try to create a Question entity along with a list of QuestionChoices, the foreign key in the QuestionChoice is coming out null.
Here is my QuestionChoice entity:
@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;
}
}
Here is my Question entity:
@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));
}
}
I have a QuestionRepository and QuestionChoiceRepository:
@Repository
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
@Repository
public interface QuestionChoiceRepository extends JpaRepository<QuestionChoice, Integer> {
}
Here is my controller:
@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();
}
}
Here is my POST request:
POST http://localhost:8080/question
Content-Type: application/json
{
"questionName": "gender",
"questionText": "What is your gender?",
"questionChoices": ["male", "female"]
}
Here is the response from the POST:
{
"id": 1,
"questionName": "gender",
"questionText": "What is your gender?",
"questionChoices": [
{
"id": 1,
"choice": "male",
"question": null
},
{
"id": 2,
"choice": "female",
"question": null
}
]
}
And here is the response from the GET request:
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": []
}
]
So not only are the foreign keys of the QuestionChoices null, but the list of question choices in the question entity is also null.
Any idea what I am doing wrong?
Update
I've found a good solution to this problem here: Infinite Recursion with Jackson JSON and Hibernate JPA issue. The problem is with Jackson, not Hibernate. Just add an additional annotation to the reference objects within the entities and everything works great!
Upvotes: 2
Views: 695
Reputation: 3370
The deserializer will always use the default constructor to construct the object. Your custom constructor has no effect over the deserialization.
What you can do is:
1 - Guarantee the association in your service / controller layer
@PostMapping("/question")
public Question createQuestion(@RequestBody Question question) {
question.getQuestionChoices().forEach(choice -> choice.setQuestion(question));
return questionRepository.save(question);
}
or 2 - Guarantee the association in your setter method:
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;
}
}
Update
To prevent the infinite recursion, simply remove the 'question' attribute from the 'questionChoice' for presentation purposes.
I can think of two options:
1 - Set the question
to null inside of questionChoice
@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;
}
This will save your question choices and foreign keys into the database, but will serialize questionChoices.question
as null when sending the response to prevent infinite recursion.
2 - Use of DTOs.
You create a DTOs to serialize them as response objects to return exactly what you want to.
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..
}
Then in your controller:
@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));
});
}
I always prefer the latter, because for big projects, IMHO, the use of DTO's can be a strong tool for organizing code and making concise use of request / response objects without using your domain objects.
Upvotes: 0
Reputation: 825
You don't use your constructor public Question(...) after request. You should make a method to link choices with question
Upvotes: 0
Reputation: 691625
You're sending an array of strings for your questionChoices
in the JSON body. Your JSON mapper needs to populate a List<Question>
from this array of strings. So it needs to transform each String
into a QuestionChoice
object. Presumably, it does that by calling the QuestionChoice
constructor that takes a String
as argument.
So you're saving a Question
which has QuestionChoices
which all have a null question
property. So you're telling JPA that all QuestionChoices don't have any question (since it's null). So JPA saves what you tell it to save: QuestionChoices without any parent question.
You need to properly initialize the question
property of the QuestionChoice
.
Upvotes: 1