0

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();
}
im_infamous
  • 972
  • 1
  • 17
  • 29

1 Answers1

0

So the answer were quite trivial all the way long.

In order to overcome this the only thing that should be done is to change container: Set<OptionEntity> to List<OptionEntity>.

Hope this will not produce some hard-to-tackle bugs but if it can - please add comment.

Because in my case uniqueness was not strict requirement, ended up with this:

PollEntity:

@Table(name = "\"poll\"")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
public class PollEntity {

    @Transient
    public OptionEntity addOption(OptionEntity pollOptionEntity) {
        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 = "status")
    @Enumerated(EnumType.STRING)
    @NotNull
    private Status status;
    @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 List<OptionEntity> options = new ArrayList<>();
}
im_infamous
  • 972
  • 1
  • 17
  • 29