Spring rest + JPA + H2 @ManyToOne 双向关系。无法存储子实体
Spring rest + JPA + H2 @ManyToOne bidirectional relationship. Unable to store child entities
我在尝试使用 JPA 存储一些实体时遇到了一些麻烦,情况如下:
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
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;
}
@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());
}
}
堆栈跟踪
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)
...
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;
这会将外键约束设置为不为空
- 保持约束并注入持久的 WebMessageEntity
- 保留约束并注入 none 持久化 WebMessageEntity,但您需要将 WebMessageEntity 端的级联设置为持久化(或全部)
- 通过删除@NotNull 更改约束
我在尝试使用 JPA 存储一些实体时遇到了一些麻烦,情况如下:
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
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; }
@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()); } }
堆栈跟踪
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) ...
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;
这会将外键约束设置为不为空
- 保持约束并注入持久的 WebMessageEntity
- 保留约束并注入 none 持久化 WebMessageEntity,但您需要将 WebMessageEntity 端的级联设置为持久化(或全部)
- 通过删除@NotNull 更改约束