Entities created correctly from Json with @JoinColumn on the opposite side

508 views Asked by At

I thought I understood the JPA's @JoinColumns annotation and mappedBy parameter, but then I needed to create new entities from this Json of a Question. It has a set of answer choices which need to be mapped to new entities as well. I decided that the Question entity is gonna be the owning side, therefore I omitted the mappedBy parameter. When I used the @JoinColumns annotation on the AnswerChoice side, all the entities were created from the Json objects, but the AnswerChoices' FKs to the Question entity were not set.

Putting the @JoinColumns in the Question entity solved the problem, but my question is: is this the correct way? Will I be facing any side effects? Should I instead have run a for-loop on the set of AnswerChoices and set the FK?

Question Json

{
    "text": "Do you know JPA?",
    "answerChoices": [{
        "text": "yes",
    }, {
        "text": "no",
    }, ]
}

Controller with a JpaRepository:

@PostMapping("/questions/create")
@ResponseBody
public String create(@RequestBody Question json) {
    questionRepo.save(json);
}

Question entity:

@Entity
public class Question {
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name="question_id")
    private Set<AnswerChoice> answerChoices;
}

AnswerChoice entity:

@Entity
public class AnswerChoice {
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnore
    private Question question;
}

For the sake of brevity I omitted the auto-generated Id's.

2

There are 2 answers

3
crizzis On BEST ANSWER

Nope, the mapping is not correct. It actually creates two separate associations that happen to share the join column.

Either remove question from Answer, making the association unidirectional (ask yourself if you really need that side of the association) or go back to the original solution and use @JsonBackReference/@JsonManagedReference (so that the field gets automatically populated during deserialization).

0
silentsudo On

As told by @crizzis, your mapping is not correct, usually, the child is at the owning side of the relationship(when 1-to-many is big with a single domain), but in your case, Question is the owning side of the relationship since you have @JoinColumn. So you can completely get rid of Question reference from AnswerChoice. When you create Question with Answer choices hibernate will

  • create questions
  • create answers
  • updates answers foreign key to questions

If you remove this line @JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_question_id")) from Question entity, hibernate will create extra table to manage this relationship called question_answer_choices hence to get rid off extra table we manually specify which column will be reference in AnswerChoice to map to foreign key.

Entity Question.java

@Entity
@Table(name = "question")
public class Question {
    @Id
    @GeneratedValue
    @Type(type = "uuid-char")
    private UUID id;

    private String description;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_question_id"))
    private Set<AnswerChoice> answerChoices = new HashSet<>();

    public UUID getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void addAnswerChoice(AnswerChoice answerChoice) {
        if (answerChoice != null) {
            this.answerChoices.add(answerChoice);
        }
    }

    public Set<AnswerChoice> getAnswerChoices() {
        return answerChoices;
    }
}

Entity AnswerChoice.java

@Entity
@Table(name = "answer_choice")
public class AnswerChoice {
    @Id
    @GeneratedValue
    @Type(type = "uuid-char")
    private UUID id;

    private String content;

    public AnswerChoice() {
    }

    public AnswerChoice(String content) {
        this.content = content;
    }


    public UUID getId() {
        return id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Test code below

    @Test
    public void testQuestionAndAnswersTest() {
        Question question = new Question();
        question.setDescription("How is the weather today?");

        question.addAnswerChoice(new AnswerChoice("Sunny"));
        question.addAnswerChoice(new AnswerChoice("Cloudy"));
        question.addAnswerChoice(new AnswerChoice("Rainy"));
        question.addAnswerChoice(new AnswerChoice("Windy"));
        question.addAnswerChoice(new AnswerChoice("Snowy"));

        //child entities persisted together
        entityManager.persist(question);


        Question searchedQuestion = entityManager.find(Question.class, question.getId());
        Assertions.assertNotNull(searchedQuestion);
        Assertions.assertNotNull(searchedQuestion.getId());
        Assertions.assertNotNull(searchedQuestion.getAnswerChoices());
        Assertions.assertEquals(5, searchedQuestion.getAnswerChoices().size());
        Set<AnswerChoice> answerChoices = searchedQuestion.getAnswerChoices();
        for (AnswerChoice answerChoice : answerChoices) {
            Assertions.assertNotNull(answerChoice.getId());
        }
    }

Table statements generated are as below:

questions.sql

CREATE TABLE `question` (
  `id` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

answer_choice.sql

CREATE TABLE `answer_choice` (
  `id` varchar(255) NOT NULL,
  `content` varchar(255) DEFAULT NULL,
  `question_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_question_id` (`question_id`),
  CONSTRAINT `fk_question_id` FOREIGN KEY (`question_id`) REFERENCES `question` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci