2

I need to implement JPA Soft Delete repository and support JPA auditing for several columns at the same time. For now, I've implemented Soft Delete repository via EL and @Query+@Modifying annotations:

    @Override
    @Modifying
    @Query("update #{#entityName} e set e.active = false where e.id = ?1")
    void deleteById(UUID id);

    @Override
    default void delete(T entity)
    {
        deleteById(entity.getId());
    }

    @Override
    @Modifying
    @Query("update #{#entityName} e set e.active = false where e.id in ?1")
    void deleteAll(Iterable<? extends T> iterable);

    @Override
    @Modifying
    @Query("update #{#entityName} e set e.active = false")
    void deleteAll();

But with such implementation audit columns are not updated because if I correctly understand, @Query annotation doesn't trigger any Lifecycle Callback methods.

@Where annotation on the Entity level is not an option because there is the need to have a possibility to query soft deleted entities.

Could you please help with any other possible solutions?

  • Where did you read/learn that the @Query won't trigger LC callbacks? – aksappy Oct 08 '19 at 16:10
  • Well, I've tried it on my own and both JPA ```AuditingEntityListener``` and my custom audit listener hadn't been called (```@PreUpdate``` method) in case of delete methods above. And also I've found this question on StackOverflow: https://stackoverflow.com/questions/56823730/spring-data-jpa-auditing-not-working-for-the-jparepository-update-method-with-m – Maksim Rudz Oct 08 '19 at 21:28

2 Answers2

1

If you are using Hibernate then you can customise the SQL executed on remove so that rather than issuing a delete statement it sets the active flag to false. In this scenario you would then be invoking EntityManager#remove (via Spring Data's repository abstraction) and lifecycle listeners would then execute as expected.

@SQLDelete(sql = "UPDATE someEntity SET active= 0 WHERE id = ?", 
                    check ResultCheckStyle.COUNT)
@Entity
public class SomeEntity{

    //if SomeChildEntity has similar @SqlDelete clause then would be 'deleted' also
    @OneToMany(cascade = CascadeType.REMOVE)
    private Set<SomeChildEntity> children;
}

This has the added advantage that cascading deletes should also execute as expected when they wouldn't when using a bulk delete.

Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • Thank you for your answer. I've seen this annotation and I haven't considered it as an option because it's impossible to use this annotation on the base ```MappedSuperClass``` entity, because this annotation requires table name. – Maksim Rudz Oct 08 '19 at 21:17
  • I think that I'll try to make delete methods default ones, where I'll update active flag column and call common save repository method. – Maksim Rudz Oct 08 '19 at 21:30
1

UPDATE: I've decided to go with the overridden default delete repository methods to update an active flag to 'false' and save an entity via common save() method.

    @Override
    default void deleteById(UUID id)
    {
        Assert.notNull(id, "The given id must not be null!");

        Optional<T> entity = this.findById(id);
        entity.ifPresent(this::delete);
    }

    @Override
    default void delete(T entity)
    {
        Assert.notNull(entity, "The entity must not be null!");

        entity.setActive(Boolean.FALSE);
        this.save(entity);
    }

    @Override
    default void deleteAll(Iterable<? extends T> entities)
    {
        Assert.notNull(entities, "The given Iterable of entities must not be null!");

        for (T entity : entities)
        {
            this.delete(entity);
        }
    }

    @Override
    default void deleteAll()
    {
        for (T element : this.findAll())
        {
            this.delete(element);
        }
    }