0

I do have an entity OptionGroup with relationships to other entities. One of the relationships is making trouble: An OptionGroup has an owner (User). When I delete an OptionGroup, for some reason, the JPA provider hibernate is trying to set the owner_id of the OptionGroup to null which violates to the NotNull constraint defined for the owner field. I have no clue why hibernate is doing this, but I can see that it is doing this in the log:

2022-08-30 20:17:53.008 DEBUG 17488 --- [nio-8081-exec-1] org.hibernate.SQL                        : update option_group set description=?, option_group_name=?, owner_id=? where id=?
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Männliche Vornamen]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [20001]
2022-08-30 20:17:53.012  WARN 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23502
2022-08-30 20:17:53.012 ERROR 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: NULL value in column »owner_id« of relation »option_group« violates Not-Null-Constraint

If I would have defined cascade delete on the owner field I could imagine that hibernate might delete the owner first, set the owner in the OptionGroup to null and then delete the OptionGroup - although it does not make much sense to first the the owner to null and then delete the OptionGroup...

Do you have any idea why hibernate is setting owner_id to null? Btw. if I remove the NotNull constraint the behavior is as expected: the OptionGroup is deleted and the User (owner) remains.

This is the OptionGroupClass:

@Entity
@Table(name = "option_group"/*, uniqueConstraints = {
        @UniqueConstraint(columnNames = {  "owner_id", "option_group_name" }) }*/)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OptionGroup {

    /**
     * Id of the Option Group. Generated by the database
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * Name of the Option Group. Unique in the context of a user.
     */
    @NotBlank(message = "Option Group name is mandatory")
    @Column(name = "option_group_name")
    private String optionGroupName;

    /**
     * Description for the Option Group
     */
    private String description;

    /**
     * User that is the owner of the Option Group.
     */
    @NotNull(message = "Owner cannot be null")
    @ManyToOne(fetch = FetchType.LAZY, cascade={CascadeType.PERSIST})
    @JoinColumn(name = "ownerId")
    private User owner;

    /**
     * List of options that belong to the Option Group.
     */
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
    @NotEmpty(message = "Options cannot be empty")
    private List<Option> options;

    /**
     * List of invitations that belong to the Option Group.
     */
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
    private List<Invitation> invitations;

    @Override
    public int hashCode() {
        return Objects.hash(description, id, optionGroupName,
                options == null ? null : options.stream().map(option -> option.getId()).toList(), owner,
                invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList());
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OptionGroup other = (OptionGroup) obj;
        return Objects.equals(description, other.description) && Objects.equals(id, other.id)
                && Objects.equals(optionGroupName, other.optionGroupName)
                && Objects.equals(options == null ? null : options.stream().map(option -> option.getId()).toList(),
                        other.options == null ? null : other.options.stream().map(option -> option.getId()).toList())
                && Objects.equals(owner, other.owner)
                && Objects.equals(
                        invitations == null ? null
                                : invitations.stream().map(invitation -> invitation.getId()).toList(),
                        other.invitations == null ? null
                                : other.invitations.stream().map(invitation -> invitation.getId()).toList());
    }

    @Override
    public String toString() {
        return "OptionGroup [id=" + id + ", optionGroupName=" + optionGroupName + ", description=" + description
                + ", owner=" + owner + ", options="
                + (options == null ? null : options.stream().map(option -> option.getId()).toList()) + ", invitations="
                + (invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList())
                + "]";
    }
}

As you can see the cascade of owner is limited to persist. f a OptionGroup is created, the owner User is created as well. But if an OptionGroup is deleted the owner User should not be deleted.

This is the User class:

/**
 * Entity that represents a user
 * 
 * Primary key: id
 */
@Entity
@Table(name = "usert", uniqueConstraints = {
        @UniqueConstraint(columnNames = { "email"}) })
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    /**
     * Id of the User. Generated by the database
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * Email address of the invitee.
     */
    @Email(message = "Email is not valid", regexp = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])")
    private String email;

    /**
     * Option Groups of which the user is the owner.
     */
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner", orphanRemoval = true)
    private List<OptionGroup> ownedOptionGroups;

    /**
     * Invitations of the user.
     */
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "invitee", orphanRemoval = true)
    private List<Invitation> invitations;

}

And this is the class that triggers the delete

/**
 * Service related to Option Groups.
 */
@Service
@Transactional
@AllArgsConstructor
public class OptionGroupService {

    /**
     * Repository used to access Option Groups.
     */
    @Autowired
    private OptionGroupRepository optionGroupRepository;

    /**
     * Deletes the Option Group with the given id.
     * 
     * @param id Id of the Option Group to delete.
     * @throws ObjectWithNameDoesNotExistException
     * @throws ObjectWithIdDoesNotExistException
     */
    public void deleteOptionGroupById(Long id) throws ObjectWithIdDoesNotExistException {
        if (optionGroupRepository.existsById(id)) {
            optionGroupRepository.deleteById(id);
        } else {
            throw new ObjectWithIdDoesNotExistException("Option Group", id);
        }
    }

}

And the repository

public interface OptionGroupRepository extends JpaRepository<OptionGroup, Long> {}

Appreciate your help on this. Thanks.

hyperion
  • 119
  • 5
  • The problem update statement is also changing the option group name and nulling description - what is linked to those changes and could that same process be nulling out your owner? Why bother updating the row that you are about to delete? Also seems odd to include mutable fields like description in your object's hash code method – Chris Aug 30 '22 at 18:57

1 Answers1

0

The root cause was an extensive use of cascades in both, parent and child entities which led to a chain of cascades: by saving an Option Group an Invitation was saved by which again an Option Group was saved. After cleaning this up it works.

I recommend reading: Hibernate - how to use cascade in relations correctly

hyperion
  • 119
  • 5