Everybody loves Spring Data Query Methods. Let’s assume that you need to find all Persons by email. You could do

@Repository
public interface PersonRepository extends CrudRepository<Person, String> {
    List<Person> findPersonsByEmail(String email);
}

Spring Data will automatically create for you an implementation that will look for all Person instances in the corresponding table/collection that have email property equal to the given string.

Soft-deletion problem

Now let’s imagine that your repository needs to implement ‘soft-delete’ logic. That means that, when an entity is ‘deleted’, it is actually just labeled as such, so this is actually an update and not a real deletion. In such a storage, you need all the retriever methods to respect that ‘soft-delete’ property: that is, they should not return entities marked as deleted.

How do we achieve this with a Spring Data repository?

Well, we could do something like this:

    List<Person> findPersonsByEmailAndDeletedIsFalse(String email);

But this does not seem very elegant. There are two problems:

  • duplication (we must do it in each and every Query Method)
  • this is error-prone (we could forget to do it somewhere)

Solution

It turns out that for Mongo (at least, for spring-data-mongo 2.1.6) we can hack into the standard QueryLookupStrategy implementation to add the desired ‘soft-deleted documents are not visible by finders’ behavior:

public class SoftDeleteMongoQueryLookupStrategy implements QueryLookupStrategy {
    private final QueryLookupStrategy strategy;
    private final MongoOperations mongoOperations;

    public SoftDeleteMongoQueryLookupStrategy(QueryLookupStrategy strategy,
            MongoOperations mongoOperations) {
        this.strategy = strategy;
        this.mongoOperations = mongoOperations;
    }

    @Override
    public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
            NamedQueries namedQueries) {
        RepositoryQuery repositoryQuery = strategy.resolveQuery(method, metadata, factory, namedQueries);

        // revert to the standard behavior if requested
        if (method.getAnnotation(SeesSoftlyDeletedRecords.class) != null) {
            return repositoryQuery;
        }

        if (!(repositoryQuery instanceof PartTreeMongoQuery)) {
            return repositoryQuery;
        }
        PartTreeMongoQuery partTreeQuery = (PartTreeMongoQuery) repositoryQuery;

        return new SoftDeletePartTreeMongoQuery(partTreeQuery);
    }

    private Criteria notDeleted() {
        return new Criteria().orOperator(
                where("deleted").exists(false),
                where("deleted").is(false)
        );
    }

    private class SoftDeletePartTreeMongoQuery extends PartTreeMongoQuery {
        SoftDeletePartTreeMongoQuery(PartTreeMongoQuery partTreeQuery) {
            super(partTreeQuery.getQueryMethod(), mongoOperations);
        }

        @Override
        protected Query createQuery(ConvertingParameterAccessor accessor) {
            Query query = super.createQuery(accessor);
            return withNotDeleted(query);
        }

        @Override
        protected Query createCountQuery(ConvertingParameterAccessor accessor) {
            Query query = super.createCountQuery(accessor);
            return withNotDeleted(query);
        }

        private Query withNotDeleted(Query query) {
            return query.addCriteria(notDeleted());
        }
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SeesSoftlyDeletedRecords {
}

We just add ‘and not deleted’ condition to all the queries unless @SeesSoftlyDeletedRecords on the method asks to avoid it.

Then, we need the following infrastructure to plug our QueryLiikupStrategy implementation:

public class SoftDeleteMongoRepositoryFactory extends MongoRepositoryFactory {
    private final MongoOperations mongoOperations;

    public SoftDeleteMongoRepositoryFactory(MongoOperations mongoOperations) {
        super(mongoOperations);
        this.mongoOperations = mongoOperations;
    }

    @Override
    protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
            QueryMethodEvaluationContextProvider evaluationContextProvider) {
        Optional<QueryLookupStrategy> optStrategy = super.getQueryLookupStrategy(key,
                evaluationContextProvider);
        return optStrategy.map(this::createSoftDeleteQueryLookupStrategy);
    }

    private SoftDeleteMongoQueryLookupStrategy createSoftDeleteQueryLookupStrategy(QueryLookupStrategy strategy) {
        return new SoftDeleteMongoQueryLookupStrategy(strategy, mongoOperations);
    }
}

public class SoftDeleteMongoRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
        extends MongoRepositoryFactoryBean<T, S, ID> {

    public SoftDeleteMongoRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
        super(repositoryInterface);
    }

    @Override
    protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
        return new SoftDeleteMongoRepositoryFactory(operations);
    }
}

Then we just need to reference the factory bean in a @EnableMongoRepositories annotation like this:

@EnableMongoRepositories(repositoryFactoryBeanClass = SoftDeleteMongoRepositoryFactoryBean.class)

If it is required to determine dynamically whether a particular repository needs to be ‘soft-delete’ or a regular ‘hard-delete’ repository, we can introspect the repository interface (or the domain class) inside our repository factory bean and decide whether we need to change the QueryLookupStrategy or not.

Predefined methods

findAll() and so on

Of course, do not forget about retrievers/finders defined in MongoRepository (and its superinterfaces). To do it, you need to implement a custom implementation of a repository as it is described in the documentation I’m not describing it here as it seems pretty straightforward.

deleteXXX() methods

Also, deletion logic needs to be changed to make an update (‘mark as deleted’) instead of an actual deletion. Same as above, this is done via custom repository implementation.

How about reactive repositories?

Absolutely the same. You just need to use reactive counterparts (ReactiveMongoOperations, ReactiveMongoRepository and so on).

Other Spring Data modules?

A follow-up that explains how to achieve the same for Elasticsearch and Spring Data Elasticsearch