Handling Soft-Deletes with Spring JPA

Handling soft-deletes with Spring JPA

This is an old question, and you probably already found the answer. BUT, for all the Spring/JPA/Hibernate programmers out there seeking for answer -

Say you have an entity Dog:

 @Entity
public class Dog{

......(fields)....

@Column(name="is_active")
private Boolean active;
}

and a repository:

public interface DogRepository extends JpaRepository<Dog, Integer> {
}

All you need to do is add the @Where annotation on the entity level, resulting:

@Entity
@Where(clause="is_active=1")
public class Dog{

......(fields)....

@Column(name="is_active")
private Boolean active;
}

All the queries performed by the repository will automatically filter out the "non-active" rows.

How to handle soft-delete in Spring Data JDBC?

I currently see three options how one might achieve this.

  1. Use views. For each aggregate root create a database view that filters out the soft-deleted rows. Map your entities against these views.

  2. write your own SqlGenerator. And inject it into the DefaultDataAccessStrategy via a SqlGeneratorSource. Since SqlGenerator is only package-visible you'll have to create your own DefaultDataAccessStrategy for this basically duplicating the existing one. This, of course, will come with a long-term cost of maintaining it.

  3. Since all you seem to need for your scenario is the very special SpEL support for the entity name opening an issue for that and submitting a pull request might be a viable option. If you are interested in this approach and need more help with it mention it in the issue description.

Spring Data Rest - Soft Delete

Try to create a custom repository, to see how it would play out

http://docs.spring.io/spring-data/jpa/docs/1.9.0.RELEASE/reference/html/#repositories.custom-implementations

But delete is not the only place you'll need to change your logic.
I see 2 ways to handle the flag requirement:

  1. Have an extra flag in your entity definition, and update it on Delete.

    In this case you need to be careful, and rewrite all existing queries, to be sure, that removed entities would not be returned, and keep in mind this separation of results, for all future entities. (Although you can hack SpringData on low level, and append this flag automatically).

  2. Delete entity from original collection and add it to another collection, where entities are stored before complete disposal.

    In this case you'll need to have additional logic for managing disposal collections, but this has no implications on query logic. You can integrate with your existing application, by adding entity listener to your JPA definition (http://docs.spring.io/spring-data/jpa/docs/1.9.0.RELEASE/reference/html/#jpa.auditing)

Change DB design to improve Spring Data JPA soft delete and authentication on queries

Ok, I will answer my own question. So, the question was:

The point is if I put an association with Professor in all classes I can make that generic code work, save tons of code but sacrificing database design's normalization. The question is: Is there another way? or maybe is it valid to do it?

And I actually did, based on this answer and it reduced a lot of code.

Now I have my model classes like this (Included Professor in all other classes too):

@MappedSuperclass
public class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Column(columnDefinition = "boolean default true", nullable = false)
private boolean enabled = true;
// ...
}

@Entity
public class Course extends AbstractEntity {
private String name;
@ManyToOne(optional = false)
private Professor professor;
//...
}

@Entity
public class Question extends AbstractEntity {
private String title;
@ManyToOne(optional = false)
private Course course;
@ManyToOne(optional = false)
private Professor professor;
// ...
}

And a CustomPagingAndSortRepository like this

@NoRepositoryBean
public interface CustomPagingAndSortRepository<T extends AbstractEntity, ID extends Long>
extends PagingAndSortingRepository<T,ID> {
@Override
@Query("select e from #{#entityName} e where e.professor = ?#{principal.professor} and e.enabled = true")
Iterable<T> findAll(Sort sort);

@Override
@Query("select e from #{#entityName} e where e.professor = ?#{principal.professor} and e.enabled = true")
Page<T> findAll(Pageable pageable);

@Override
@Query("select e from #{#entityName} e where e.id =?1 and e.professor = ?#{principal.professor} and e.enabled = true")
T findOne(Long id);

@Override
default boolean exists(Long id){
return findOne(id) != null;
}

@Override
@Query("select e from #{#entityName} e where e.professor = ?#{principal.professor} and e.enabled = true")
Iterable<T> findAll();

@Override
@Query("select e from #{#entityName} e where e.professor = ?#{principal.professor} and e.enabled = true")
Iterable<T> findAll(Iterable<ID> iterable);

@Override
@Query("select count(e) from #{#entityName} e where e.professor = ?#{principal.professor} and e.enabled = true")
long count();

@Override
@Transactional
@Modifying
@Query("update #{#entityName} e set e.enabled=false where e.id=?1 and e.professor = ?#{principal.professor}")
void delete(Long id);

@Override
@Transactional
@Modifying
default void delete(T t){
delete(t.getId());
}

@Override
@Transactional
@Modifying
default void delete(Iterable<? extends T> iterable){
iterable.forEach(entity -> delete(entity.getId()));
}

@Override
@Transactional
@Modifying
@Query("update #{#entityName} e set e.enabled=false where e.professor = ?#{principal.professor}")
void deleteAll();
}

Doing this, all my basic queries will include automatically the name of the entity, the professor and if it is enabled. For example, my QuestionRepository is like this:

public interface QuestionRepository extends CustomPagingAndSortRepository<Question, Long> {
@Query("select q from Question q where q.course.id = ?1 and q.title like %?2% and q.professor = ?#{principal.professor} and q.enabled = true")
List<Question> listQuestionsByCourseAndTitle(long courseId, String title);
}

Now I don't have to worry about creating a custom delete, or custom findALL or deleteAll etc. to include Professor and enabled for every single class that I create.

So, the answer to my own question: in my opinion, it is valid to add an association with a professor in all classes (in cases like mine, using JWT Authentication). I was able to reduce a lot of code and it is a 100% Spring solution.

PS: Didn't test findAll with Sort and Pageable yet.

JPA Soft Delete Repository + Auditing

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);
}
}


Related Topics



Leave a reply



Submit