Continuing on the soft-delete repositories topic, let’s look at Elasticsearch.

It has Spring Data support. And having sorted this out for Mongo, it is quite easily to do the same for Elasticsearch.

Let’s jump to the …

Solution

Again, we can hack into the standard QueryLookupStrategy implementation to add the desired ‘soft-deleted documents are not visible by finders’ behavior:

public class SoftDeleteElasticsearchQueryLookupStrategy implements QueryLookupStrategy {
    private final QueryLookupStrategy strategy;
    private final ElasticsearchOperations elasticsearchOperations;
    private final SoftDeletion softDeletion = new SoftDeletion();

    public SoftDeleteElasticsearchQueryLookupStrategy(QueryLookupStrategy strategy,
            ElasticsearchOperations elasticsearchOperations) {
        this.strategy = strategy;
        this.elasticsearchOperations = elasticsearchOperations;
    }

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

        if (method.getAnnotation(SeesSoftlyDeletedRecords.class) != null) {
            return repositoryQuery;
        }

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

        return new SoftDeletePartTreeElasticsearchQuery(partTreeQuery);
    }

    private class SoftDeletePartTreeElasticsearchQuery extends ElasticsearchPartQuery {
        SoftDeletePartTreeElasticsearchQuery(ElasticsearchPartQuery partTreeQuery) {
            super((ElasticsearchQueryMethod) partTreeQuery.getQueryMethod(),
                    SoftDeleteElasticsearchQueryLookupStrategy.this.elasticsearchOperations);
        }

        @Override
        public CriteriaQuery createQuery(ParametersParameterAccessor accessor) {
            CriteriaQuery query = super.createQuery(accessor);
            return withNotDeleted(query);
        }

        private CriteriaQuery withNotDeleted(CriteriaQuery query) {
            return query.addCriteria(softDeletion.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 SoftDeleteElasticsearchRepositoryFactory extends ElasticsearchRepositoryFactory {
    private final ElasticsearchOperations elasticsearchOperations;

    public SoftDeleteElasticsearchRepositoryFactory(ElasticsearchOperations elasticsearchOperations) {
        super(elasticsearchOperations);
        this.elasticsearchOperations = elasticsearchOperations;
    }

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

    private SoftDeleteElasticsearchQueryLookupStrategy createSoftDeleteQueryLookupStrategy(QueryLookupStrategy strategy) {
        return new SoftDeleteElasticsearchQueryLookupStrategy(strategy, elasticsearchOperations);
    }
}

public class SoftDeleteElasticsearchRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
        extends ElasticsearchRepositoryFactoryBean<T, S, ID> {

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

    @Override
    protected RepositoryFactorySupport getFactoryInstance(ElasticsearchOperations operations) {
        return new SoftDeleteElasticsearchRepositoryFactory(operations);
    }
}

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

@EnableElasticsearchRepositories(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 (ReactiveElasticsearchOperations, ReactiveElasticsearchRepository and so on).

Other Spring Data modules?

The previous post explains how to achieve the same for MongoDB and Spring Data MongoDB