英文:
Entities created correctly from Json with @JoinColumn on the opposite side
问题
我以为我理解了JPA的*@JoinColumns注解和mappedBy参数,但后来我需要根据这个Question的Json创建新的实体Question。它有一组答案选项,也需要映射为新的实体。我决定Question实体将成为拥有方,因此我省略了mappedBy参数。当我在AnswerChoice端使用@JoinColumns*注解时,所有实体都从Json对象创建了出来,但是AnswerChoice到Question实体的外键关系没有被设置。
将*@JoinColumns*注解放在Question实体中解决了这个问题,但我的问题是:这样做是正确的方式吗?我会遇到任何副作用吗?我是否应该在AnswerChoice集合上运行for循环并设置外键呢?
问题的Json:
{
"text": "你知道JPA吗?",
"answerChoices": [{
"text": "是",
}, {
"text": "否",
}]
}
带有JpaRepository的控制器:
@PostMapping("/questions/create")
@ResponseBody
public String create(@RequestBody Question json) {
questionRepo.save(json);
}
Question实体:
@Entity
public class Question {
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name="question_id")
private Set<AnswerChoice> answerChoices;
}
AnswerChoice实体:
@Entity
public class AnswerChoice {
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore
private Question question;
}
出于简洁起见,我省略了自动生成的Id。
英文:
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.
答案1
得分: 3
不对,映射不正确。实际上它创建了两个分开的关联,这些关联碰巧共享连接列。
要么从 Answer
中移除 question
,使关联成为单向的(自己考虑是否真的需要关联的这一侧),要么返回到原始解决方案并使用 @JsonBackReference/@JsonManagedReference
(这样字段在反序列化过程中会自动填充)。
英文:
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).
答案2
得分: 3
如@crizzis所说,你的映射是不正确的,通常情况下,在一对多关系中,子项位于关系的拥有端(当一个域与多个项关联时),但在你的情况下,Question
是关系的拥有端,因为你使用了 @JoinColumn
。因此,你可以完全从 AnswerChoice
中删除 Question
的引用。当你创建带有答案选项的问题时,Hibernate 将执行以下操作:
- 创建问题
- 创建答案
- 更新答案的外键以关联到问题
如果从问题实体中删除此行 @JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_question_id"))
,Hibernate 将创建额外的表来管理这个关系,称为 question_answer_choices
,因此为了消除额外的表,我们手动指定 AnswerChoice
中将引用的外键列。
实体 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;
}
}
实体 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
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"));
// 子实体一起持久化
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());
}
}
生成的表语句如下:
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
英文:
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论