Почему hibernate создает нулевые внешние ключи? - PullRequest
2 голосов
/ 16 октября 2019

Я пытаюсь создать приложение весенней загрузки с двумя объектами: Question и QuestionChoices. Я использую двунаправленные отношения. Когда я пытаюсь создать сущность Вопрос вместе со списком Вопросов, внешний ключ в Вопросе выходит пустым.

Вот моя сущность 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;
    }

}

Вот моя сущность Question:

@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 не только нулевые, но и список вариантов вопроса в объекте вопроса также равен нулю.

ЛюбойПонимаете, что я делаю неправильно?


Обновление

Я нашел хорошее решение этой проблемы здесь: Бесконечная рекурсия с Джексоном JSON иСпящий выпуск JPA . Проблема с Джексоном, а не с Hibernate. Просто добавьте дополнительную аннотацию к ссылочным объектам внутри сущностей, и все будет прекрасно!

Ответы [ 3 ]

1 голос
/ 16 октября 2019

Вы отправляете массив строк для вашего questionChoices в теле JSON. Ваш преобразователь JSON должен заполнить List<Question> из этого массива строк. Поэтому необходимо преобразовать каждый String в QuestionChoice объект. Предположительно, он делает это, вызывая конструктор QuestionChoice, который принимает в качестве аргумента String.

Таким образом, вы сохраняете Question, который имеет QuestionChoices, и все имеют свойство null question,Итак, вы говорите JPA, что у всех QuestionChoices нет вопросов (так как они нулевые). Таким образом, JPA сохраняет то, что вы говорите, чтобы сохранить: QuestionChoices без какого-либо родительского вопроса.

Вам нужно правильно инициализировать свойство question QuestionChoice.

0 голосов
/ 16 октября 2019

Десериализатор всегда будет использовать конструктор по умолчанию для создания объекта. Ваш пользовательский конструктор не влияет на десериализацию.

То, что вы можете сделать, это:

1 - Гарантировать ассоциацию в слое вашего сервиса / контроллера

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

или 2- Гарантируйте связь в вашем методе установки:

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

Обновление

Чтобы предотвратить бесконечную рекурсию, просто удалите атрибут 'question' изquestionChoice 'для целей презентации.

Я могу представить себе два варианта:

1 - установить question на ноль внутри 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;
}

Это будетсохраните ваши варианты вопросов и внешние ключи в базе данных, но при отправке ответа сериализует 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));
    });
}

Я всегда предпочитаю последнее, потому что для больших проектов, IMHO, использование DTO может быть сильным инструментом для организации кода и краткого использования объектов запроса / ответа без использования ваших доменных объектов.

0 голосов
/ 16 октября 2019

Вы не используете ваш конструктор public Question (...) после запроса. Вы должны сделать метод, чтобы связать выбор с вопросом

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...