My problem is that Hibernate does not persist nested entities given in entity.
Consider following entities:
PollEntity
@Table(name = "\"poll\"")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@EqualsAndHashCode(of = "id")
@Builder
public class PollEntity {
@Transient
public OptionEntity addOption(OptionEntity pollOptionEntity) {
if(options == null)
options = new HashSet<>();
options.add(pollOptionEntity);
pollOptionEntity.setPoll(this);
return pollOptionEntity;
}
@Transient
public OptionEntity dropOption(OptionEntity pollOptionEntity) {
options.remove(pollOptionEntity);
pollOptionEntity.setPoll(null);
return pollOptionEntity;
}
@Id
@Column(name = "id")
private UUID id;
@Column(name = "author")
@UUIDv4
private UUID author;
@Column(name = "poll_question")
@Size(max = 1000)
private String pollQuestion;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "poll", cascade = CascadeType.MERGE)
@Builder.Default
@Valid
private Set<OptionEntity> options;
}
OptionEntity
@Table(name = "\"option\"")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@EqualsAndHashCode(of = "id")
@Builder
public class OptionEntity {
@Id
@GeneratedValue(generator = "UUID")
@Column(name = "id")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
@JoinColumn(name = "poll_id")
@ManyToOne
private PollEntity poll;
@Column(name = "option")
@Size(max = 1000)
@NotNull
private String option;
}
And here's service method:
@Transactional(rollbackFor = Throwable.class)
public void createPoll(@Valid PollEntity pollEntity) throws ValidationException {
validationService.validateOrThrow(pollEntity);
if (pollRepository.findById(pollEntity.getId()).isPresent())
throw new ValidationException("Invalid id", Map.of("id", "Poll with id (" + pollEntity.getId() + ") already exists"));
pollEntity = validationService.validateAndSave(pollRepository, pollEntity);
And corresponding test:
@Test
public void createPollTest() throws ValidationException {
var uuid = UUID.randomUUID();
var pollOption1 = OptionEntity.builder()
.option("Test option 1")
.build();
var pollOption2 = OptionEntity.builder()
.option("Test option 2")
.build();
var pollOption3 = OptionEntity.builder()
.option("Test option 3")
.build();
var poll = PollEntity.builder()
.id(uuid)
.pollQuestion("Test question")
.author(UUID.randomUUID())
.build();
poll.addOption(pollOption1);
poll.addOption(pollOption2);
poll.addOption(pollOption3);
pollService.createPoll(poll);
}
Which gives following output in database
poll
2e565f50-7cd4-4fc9-98cd-49e0f0964487 feae5781-ff07-4a21-9292-c11c4f1a047d Test question
option
c786fe5d-632d-4e94-95ef-26ab2af633e7 fc712242-8e87-41d8-93f2-ff0931020a4a Test option 1
and rest options ended up unpersisted.
I've also used to create options in separate method
@Transactional(propagation= Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
public Set<OptionEntity> createOptions(@Valid Set<OptionEntity> pollOptionsEntities) throws ValidationException {
for (var pollOption : pollOptionsEntities) {
validationService.validateAndSave(pollOptionsRepository, pollOption);
}
return pollOptionsEntities;
}
and option entities were getting produced but had to switch to persisting from built-in entity methods due to errors with persisting poll entity.
Database schema looks like this:
CREATE TABLE "poll"
(
"id" UUID PRIMARY KEY,
"author" UUID NOT NULL,
"poll_question" VARCHAR(1000) NOT NULL
)
CREATE TABLE "option"
(
"id" UUID PRIMARY KEY,
"poll_id" UUID NOT NULL REFERENCES "poll" ("id") ON DELETE CASCADE
"option" VARCHAR(1000) NOT NULL
)
What are the possible approaches to try?
UPD 1
Having considered various looking-alike questions (1,2,3,4,5)
I've came up with this addition to entity which suppose make entities persistence regardless of actual value and still having only one option in output. What was done wrong?
@Override
public boolean equals(Object object) {
if ( object == this ) {
return false;
}
if ( object == null || object.getClass() != getClass() ) {
return false;
}
final OptionEntity other = OptionEntity.class.cast( object );
if ( getId() == null && other.getId() == null ) {
return false;
}
else {
return false;
}
}
@Override
public int hashCode() {
final HashCodeBuilder hcb = new HashCodeBuilder( 17, 37 );
if ( id == null ) {
while (getOptions().iterator().hasNext())
hcb.append( getOptions().iterator().next() );
}
else {
hcb.append( id );
}
hcb.append( options );
return hcb.toHashCode();
}