【问题标题】:Persist a OneToMany entity with generated IDs in Spring Boot在 Spring Boot 中使用生成的 ID 持久化 OneToMany 实体
【发布时间】:2020-05-19 11:04:08
【问题描述】:

我有一个实体,Question,我想将其保存到数据库中。每个问题都通过 questionId 字段由一些 Answers 引用。

两个实体都有一个 ID 字段,该字段在持久化时会自动生成。以下是实体的简化代码:

Question.java

@Data
@Entity
public class Question {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    @Size(max=1000, message="Text too long")
    @NotNull(message="Field text cannot be null")
    private String text;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name="id", referencedColumnName = "questionId")
    private List<Answer> answers;
}

Answer.java

@Data
@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    @Size(max=255, message="Text too long")
    @NotNull(message="Field text cannot be null")
    private String text;

    @NotNull(message="Field questionId cannot be null")
    private Integer questionId;
}

因此,为了保留一个包含一些答案的 Question 对象,我创建了那些没有 ID 字段的对象,这些对象将自动生成。 Question 对象从 QuestionController 中的 JSON 序列化:

@RestController
@RequestMapping(value="/questions")
public class QuestionController {
    @Autowired
    private QuestionRepository questionRepository;

    @PostMapping
    public void createQuestion(@RequestBody Question question){
        questionRepository.save(question);
    }
}

问题是我还必须将 questionId 字段留空,因为在将其写入数据库之前我不知道它。这会导致事务在请求该字段的值时抛出错误。

到目前为止,我想出的唯一解决方案是从问题中删除答案,一旦保留,填写 questionId 值并分别保存答案。

有什么方法可以同时进行这些交易吗?

【问题讨论】:

  • 您不使用Integer 字段来引用问题。要么完全忽略它(使其成为从问题到答案的单向关联),要么使其成为对 Question 对象的引用(使其成为双向关联)。有关详细信息,请参阅教程。

标签: java spring-boot jpa jackson


【解决方案1】:

您应该将实体和 DTO(数据传输对象)的概念分开。

例如,您可以在控制器中创建一个新的QuestionDTO 对象作为方法的主体契约。 在您的框架解析和验证后,您将其转换(参见 org.springframework.core.convert.converter.Converter)到您的实体对象。 之后,您可以安全地保存您的实体。

@Data
public class QuestionDTO {

    @Size(max=1000, message="Text too long")
    @NotNull(message="Field text cannot be null")
    private String text;

    private List<AnswerDTO> answers;
}
@Data
public class AnswerDTO {

    @Size(max=255, message="Text too long")
    @NotNull(message="Field text cannot be null")
    private String text;

}
@RestController
@RequestMapping(value="/questions")
public class QuestionController {
    @Autowired
    private QuestionRepository questionRepository;

    @PostMapping
    public void createQuestion(@RequestBody QuestionDTO questionDTO){
        Question question = convert(questionDTO);
        questionRepository.save(question);
    }
}

【讨论】:

    【解决方案2】:

    根据我的经验,您只是以错误的方式为实体建模。 正如另一条评论中所说,了解您的用例将是什么很重要。 例如,当您对问题/答案进行建模时,在很多情况下您将使用您的问题,并且只使用问题,并使用与答案的关系来显示它们。但是从答案中访问问题不太可能发生......

    那么,你是怎么做到的呢?您听说过聚合根和值对象的概念吗?这很容易。在这里你可以如何设计你的关系问题/答案:

    1. 创建一个Answer 类,并使用@Embeddable 注释。因此,hibernate 不会尝试创建一个表来存储您的答案,但您可以使用它来帮助 hibernate 映射最佳方式。
    @Data
    @Embeddable
    public class Answer {
        private String text;
    }
    

    就是这样,这就是您代表值对象Answer所需要的一切。

    1. 使用Answers 列表创建您的实体Question(注意我删除了您的约束注释,但请随意使用它们):
    @Data
    @Entity
    public class Question {
        @Id
        @GeneratedValue(strategy= GenerationType.IDENTITY)
        private Integer id;
    
        private String text;
    
        @ElementCollection(fetch = FetchType.EAGER)
        @AttributeOverride(name = "text", column = @Column(name = "answer_text"))
        private List<Answer> answers;
    }
    
    • @ElementCollection(fetch = FetchType.EAGER) 是 Hibernate 必须知道它需要存储 Answer 的列表,因此创建一个表来存储您的问题的无限数量的答案。
    • @AttributeOverride(name = "text", column = @Column(name = "answer_text")) 在这里对 Hibernate 说:请存储我的 Answer 类的属性 text 的值,并将此值存储到名称为 answer_text 的列中。

    接下来要做什么?创建ControllerRepository 并尝试发布问题。

    @RestController
    public class QuestionController {
    
        @Autowired
        private QuestionRepository repository;
    
        @PostMapping("/questions")
        public void postQuestion(@RequestBody Question question) {
            repository.save(question);
        }
    
        @GetMapping("/questions")
        public Iterable<Question> questions() {
            return repository.findAll();
        }
    }
    
    public interface QuestionRepository extends CrudRepository<Question, Integer> {}
    

    最后,发表你的问题:

    {
        "text": "quetion text", 
        "answers": [{
            "text": "answer1"
        }, {
            "text": "answer2"
        }]
    }
    

    这将创建您的问题及其答案列表。

    即使我将在下面列出优点/缺点,我仍然认为这是您可以拥有的最佳解决方案/妥协:

    优点

    • 易于配置
    • 您不必管理QuestionAnswer 之间的链接
    • 当您获取问题时,它也会获取关联的Answer

    缺点

    • 您无法修改特定问题的特定答案(但您会修改吗?)
    • 您没有一个表格来代表您数据库中的所有答案(但再一次,您会不会)

    给定基本用例,试试这个。

    正如您所意识到的,这里是 hibernate 使用此解决方案创建的表:

    Hibernate:创建表问题(默认生成的id整数为标识(以1开头),文本varchar(255),主键(id))
    Hibernate:创建表question_answers(question_id integer not null, answer_text varchar(255))

    只有两张表,一张question和一张** question_answers**...

    【讨论】:

      【解决方案3】:

      想想典型的工作流程是什么:

      您首先创建一个问题(任何可能保留它),其中包含一个空的答案数组。

      然后你一一添加答案,问题已经被持久化,因此有一个ID。而不是 questionId 上的@NotNull,我宁愿添加一个@ManyToOne,它可以正确地指示休眠状态如何使用该字段。

      但即使没有先保留问题,由于您添加了级联类型,休眠应该在您调用问题保存后立即正确解决问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-06-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-26
        • 1970-01-01
        • 2017-11-29
        相关资源
        最近更新 更多