Spring rest + JPA + H2 @ManyToOne 双向关系。无法存储子实体

Spring rest + JPA + H2 @ManyToOne bidirectional relationship. Unable to store child entities

我在尝试使用 JPA 存储一些实体时遇到了一些麻烦,情况如下:

  1. WebMessageEntity.java

    @EqualsAndHashCode
    @Data
    @Entity(name = "web_message")
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class WebMessageEntity{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Getter
    @Column(name = "WEB_MESSAGE_ID")
    private Long id;
    
    @Getter
    @Setter
    @NotEmpty
    private String hotelTicker;
    
    @Getter
    @Setter
    @NotNull
    @Enumerated(EnumType.STRING)
    private WebMessageColor color;
    
    @Getter
    @Setter
    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "message_type")
    private WebMessageType type;
    
    @Getter
    @Setter
    @NotNull
    @Enumerated(EnumType.STRING)
    private ReservationStep step;
    
    @Getter
    @Setter
    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "message_trigger")
    private WebMessageTrigger trigger;
    
    private int duration;
    
    @Enumerated(EnumType.STRING)
    private WebMessagePosition position;
    
    @Getter
    @Setter
    @NotNull
    private boolean isActive;
    
    @Getter
    @Setter
    @NotNull
    @Convert(converter = LocalDateAttributeConverter.class)
    private LocalDate startDate;
    
    @Getter
    @Setter
    @NotNull
    @Convert(converter = LocalDateAttributeConverter.class)
    private LocalDate endDate;
    
    @Setter
    @OneToMany(mappedBy = "webMessage", cascade = CascadeType.ALL)
    @NotNull
    private List<WebMessageTranslationEntity> translations;
    
    @Getter
    @Setter
    @NotEmpty
    private String userName;
    
    @Getter
    @Setter
    @NotNull
    @Convert(converter = LocalDateTimeAttributeConverter.class)
    private LocalDateTime creationDate;
    
    @Getter
    @Setter
    private String modifiedBy;
    
    @Getter
    @Setter
    @Convert(converter = LocalDateTimeAttributeConverter.class)
    private LocalDateTime modificationDate;
    
    
    //constructors
    //------------------------------------------------------------------------------------------------------
    
    
    
    private WebMessageEntity(String hotelTicker, WebMessageColor color, WebMessageType type, ReservationStep step,
                             WebMessageTrigger trigger, boolean isActive, LocalDate startDate, LocalDate endDate,
                             List<WebMessageTranslationEntity> translations, String userName, LocalDateTime creationDate,
                             String modifiedBy, LocalDateTime modificationDate)
    {
        this.hotelTicker = hotelTicker;
        this.color = color;
        this.type = type;
        this.step = step;
        this.trigger = trigger;
        this.isActive = isActive;
        this.startDate = startDate;
        this.endDate = endDate;
        this.translations = translations;
        this.userName = userName;
        this.creationDate = creationDate;
        this.modifiedBy = modifiedBy;
        this.modificationDate = modificationDate;
    }
    
    private WebMessageEntity(String hotelTicker, WebMessageColor color, WebMessageType type, ReservationStep step,
                            WebMessageTrigger trigger, int duration, WebMessagePosition position, boolean isActive,
                            LocalDate startDate, LocalDate endDate, List<WebMessageTranslationEntity> translations,
                            String userName, LocalDateTime creationDate, String modifiedBy, LocalDateTime modificationDate)
    {
        this.hotelTicker = hotelTicker;
        this.color = color;
        this.type = type;
        this.step = step;
        this.trigger = trigger;
        this.setDuration(duration);
        this.setPosition(position);
        this.isActive = isActive;
        this.startDate = startDate;
        this.endDate = endDate;
        this.translations = translations;
        this.userName = userName;
        this.creationDate = creationDate;
        this.modifiedBy = modifiedBy;
        this.modificationDate = modificationDate;
    }
    
    //GETTERS, SETTERS and some private field verification methods
    
  2. WebMessageTranslationEntity

    @Data
    @EqualsAndHashCode
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Entity(name = "web_message_translation")
    @Table(uniqueConstraints = @UniqueConstraint(columnNames =                 {"locale", "message_id"}))
    public class WebMessageTranslationEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Getter
        @Column(name = "MESSAGE_TRANSLATION_ID")
        private Long id;
    
        @NotEmpty
        private String content;
    
        @NotEmpty
        private String locale;
    
        @ManyToOne
        @JoinColumn(name = "MESSAGE_ID")
        private WebMessageEntity webMessage;
    
    }
    
  3. @Controller

    @RestController
    @RequestMapping("/api")
    public class WebMessageResource {
    
    
        private final WebMessageService messageService;
    
        @Autowired
        public WebMessageResource(WebMessageService messageService) {
        this.messageService = messageService;
        }
    
    
        @PostMapping("{hotelTicker}/messages")
        public ResponseEntity<?> createMessage(@RequestBody @Valid     WebMessageDTO dto, @PathVariable @NotNull String hotelTicker) {
    
    
            if (verifyHotelTicker(dto, hotelTicker)) {
    
                WebMessageEntity newEntity =         messageService.store(fromWebMessageDTOToEntity(dto));
    
                HttpHeaders headers = new HttpHeaders();
    
                //TODO rebuild URI with exact path to access resource
            headers.setLocation(ControllerLinkBuilder.linkTo(FilterEntity.class).slash(newEntity.getHotelTicker()).slash(newEntity.getId()).toUri());
    
            return new ResponseEntity<>(fromWebMessageEntityToDTO(newEntity), headers, HttpStatus.CREATED);
    
        }
    
        return new ResponseEntity<>("Hotel ticker specified in URI doesn't match with DTO's hotel ticker", HttpStatus.BAD_REQUEST);
    
    }
    
    private boolean verifyHotelTicker(WebMessageDTO dto, String hotelTicker) {
        return hotelTicker.equals(dto.getHotelTicker());
    }
    
    private List<WebMessageTranslationEntity> fromTranslationDTOsToEntities(List<WebMessageTranslationDTO> translationDTOs) {
    
        return translationDTOs
                .stream()
                .map(translation -> WebMessageTranslationEntity
                        .builder()
                        .content(translation.getContent())
                        .locale(translation.getLocale())
                        .build())
                    .collect(toList());
    
        }
    
        private WebMessageEntity fromWebMessageDTOToEntity(WebMessageDTO webMessageDTOs) {
    
            return WebMessageEntity
                    .builder()
                    .hotelTicker(webMessageDTOs.getHotelTicker())
                    .color(webMessageDTOs.getColor())
                    .type(webMessageDTOs.getType())
                    .step(webMessageDTOs.getStep())
                    .trigger(webMessageDTOs.getTrigger())
                    .duration(webMessageDTOs.getDuration())
                    .position(webMessageDTOs.getPosition())
                    .isActive(webMessageDTOs.getIsActive())
                    .startDate(webMessageDTOs.getStartDate())
                    .endDate(webMessageDTOs.getEndDate())
                    .userName(webMessageDTOs.getUserName())
                    .creationDate(webMessageDTOs.getCreationDate())
                    .modifiedBy(webMessageDTOs.getModifiedBy())
                    .modificationDate(webMessageDTOs.getModificationDate())
                    .translations(this.fromTranslationDTOsToEntities(webMessageDTOs.getTransla    tions()))
                    .build();
    }
    
    private WebMessageDTO fromWebMessageEntityToDTO(WebMessageEntity webMessageEntity) {
    
        return WebMessageDTO
                .builder()
                .id(webMessageEntity.getId())
                .hotelTicker(webMessageEntity.getHotelTicker())
                .color(webMessageEntity.getColor())
                .type(webMessageEntity.getType())
                .step(webMessageEntity.getStep())
                .trigger(webMessageEntity.getTrigger())
                .duration(webMessageEntity.getDuration())
                .position(webMessageEntity.getPosition())
                .isActive(webMessageEntity.isActive())
                .startDate(webMessageEntity.getStartDate())
                .endDate(webMessageEntity.getEndDate())
                .userName(webMessageEntity.getUserName())
                .creationDate(webMessageEntity.getCreationDate())
                .modifiedBy(webMessageEntity.getModifiedBy())
                .modificationDate(webMessageEntity.getModificationDate())
                .translations(this.fromTranslationEntitiesToDTO(webMessageEntity.getTranslations()))
                .build();
    }
    
    private List<WebMessageTranslationDTO> fromTranslationEntitiesToDTO(List<WebMessageTranslationEntity> translationEntities) {
    
        return translationEntities
                .stream()
                .map(translation -> WebMessageTranslationDTO
                        .builder()
                        //.id(translation.getId())
                        .content(translation.getContent())
                        .locale(translation.getLocale())
                        .build())
                .collect(toList());
    }
    
    
    }
    
  4. 堆栈跟踪

    org.h2.jdbc.JdbcSQLException: NULL not allowed for column     
    "MESSAGE_ID"; SQL statement:
    insert into web_message_translation (message_translation_id, content,locale, message_id) values (null, ?, ?, ?) [23502-194]
        at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
        at org.h2.message.DbException.get(DbException.java:179)
        at org.h2.message.DbException.get(DbException.java:155)
        at org.h2.table.Column.validateConvertUpdateSequence(Column.java:311)
        at org.h2.table.Table.validateConvertUpdateSequence(Table.java:793)
        at org.h2.command.dml.Insert.insertRows(Insert.java:151)
        at org.h2.command.dml.Insert.update(Insert.java:114)
        at org.h2.command.CommandContainer.update(CommandContainer.java:101)
        at org.h2.command.Command.executeUpdate(Command.java:258)
        ...
    
  5. Table 创建 YAML 文件

    - changeSet:
      id: '008-1'
      author: arnau
      comment: 'create table web_message'
      preConditions:
        - onFail: MARK_RAN
        - onFailMessage: 'Table already exists, must be production environment...'
        - not:
          - tableExists:
              tableName: web_message
      changes:
      - createTable:
          tableName: web_message
          columns:
          - column:
              name: WEB_MESSAGE_ID
              type: NUMBER
              autoIncrement: true
              constraints:
                primaryKey: true
                nullable: false
          - column:
              name: hotel_ticker
              type: VARCHAR(155)
              constraints:
                nullable: false
          - column:
              name: color
              type: VARCHAR(255)
              constraints:
                nullable: false
          - column:
              name: message_type
              type: VARCHAR(255)
              constraints:
                nullable: false
          - column:
              name: step
              type: VARCHAR(255)
              constraints:
                nullable: false
          - column:
              name: message_trigger
              type: VARCHAR(255)
              constraints:
                nullable: false
          - column:
              name: duration
              type: NUMBER
              constraints:
                nullable: true
          - column:
              name: position
              type: VARCHAR(255)
              constraints:
                nullable: true
          - column:
              name: is_active
              type: BOOLEAN(1)
              constraints:
                nullable: false
          - column:
              name: start_date
              type: DATE
              constraints:
                nullable: false
          - column:
              name: end_date
              type: DATE
              constraints:
                nullable: false
          - column:
              name: user_name
              type: VARCHAR(255)
              constraints:
                nullable: false
          - column:
              name: creation_date
              type: TIMESTAMP
              constraints:
                nullable: false
          - column:
              name: modified_by
              type: VARCHAR(255)
              constraints:
                nullable: true
          - column:
              name: modification_date
              type: TIMESTAMP
              constraints:
                nullable: true
    
    
    - changeSet:
          id: '008-2'
          author: arnau
          comment: 'create table web_message_translation'
          preConditions:
            - onFail: MARK_RAN
            - onFailMessage: 'Table already exists, must be production environment...'
            - not:
              - tableExists:
                  tableName: web_message_translation
          changes:
          - createTable:
              tableName: web_message_translation
              columns:
              - column:
                  name: MESSAGE_TRANSLATION_ID
                  type: NUMBER
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: locale
                  type: VARCHAR(2)
                  constraints:
                    unique: true
                    nullable: false
              - column:
                  name: content
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
              - column:
                  name: message_id
                  type: CHAR(22)
                  constraints:
                    nullable: false
                    references: web_message(WEB_MESSAGE_ID)
                    foreignKeyName:     fk_web_message_translation__web_message
      - changeSet:
          id: '008-3'
          author: arnau
          comment: 'Add unique constraint by locale and web_message_id'
          changes:
          - addUniqueconstraint:
            tableName: web_message_translation
            columnNames: locale, message_id
            constraintName: uc_locale__message_id
    

问题是,当我尝试存储这些实体时,JPA 尝试在列 message_id 中存储具有 NULL 值的实体 webMessageTranslationEntity,显然,这被数据库拒绝了,因为这字段设置为 NOT NULL。

我如何存储具有这些必要条件的实体?

@NotNull
private List<WebMessageTranslationEntity> translations;

这会将外键约束设置为不为空

  1. 保持约束并注入持久的 WebMessageEntity
  2. 保留约束并注入 none 持久化 WebMessageEntity,但您需要将 WebMessageEntity 端的级联设置为持久化(或全部)
  3. 通过删除@NotNull 更改约束