简体   繁体   中英

Spring mongo soft delete

I need to implement soft delete functionality(Maintain a boolean field in table and filter all query based on this). Below link has solution for hibernate only. Handling soft-deletes with Spring JPA

Since my application is very old, I don't want to change each existing query. I am looking for solution like one place change in spring data classes. Spring mongo data version: 1.5.0.RELEASE

Add Boolean Field active to every class which is mapped with Collection
set the same true for all valid Documents  and false for non valid documnets 
private Boolean active = Boolean.TRUE;

and can chnage your Query to 
Long countByActiveTrueAndAccountStatusNot(AccountStatus status);

First Step. Override default methods like as findAll(), findById(), exists()... . For this you should override mongoTemplate, it simple).

  1. Add to your entities field "deletedAt":
@Document("costAreas")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
public class User{
    @Id
    String id;
    String name;
    LocalDateTime deletedAt;
}

PS: Filed "deletedAt" contains the date of deletion (if this field is null then document wasn't deleted).

  1. Create CustomMongoTemplate:
import com.mongodb.client.MongoClient;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;

public class CustomMongoTemplate extends MongoTemplate {
    public CustomMongoTemplate(MongoTemplate mongoTemplate) {
        super(mongoTemplate.getMongoDatabaseFactory());
    }

    public CustomMongoTemplate(MongoClient mongoClient, String databaseName) {
        super(mongoClient, databaseName);
    }

    public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory) {
        super(mongoDbFactory);
    }

    public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) {
        super(mongoDbFactory, mongoConverter);
    }

    @Override
    public <T> List<T> find(Query query, Class<T> entityClass, String collectionName) {
        Assert.notNull(query, "Query must not be null!");
        Assert.notNull(collectionName, "CollectionName must not be null!");
        Assert.notNull(entityClass, "EntityClass must not be null!");
        query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));

        return super.find(query, entityClass, collectionName);
    }

    @Nullable
    @Override
    public <T> T findById(Object id, Class<T> entityClass, String collectionName) {
        T t = super.findById(id, entityClass, collectionName);

        try {
            Field field = entityClass.getDeclaredField("deletedAt");
            field.setAccessible(Boolean.TRUE);
            if (Objects.nonNull(field.get(t))) {
                return null;
            }
        } catch (NoSuchFieldException | IllegalAccessException ignored) {
        }

        return t;
    }

    @Nullable
    @Override
    public <T> T findOne(Query query, Class<T> entityClass, String collectionName) {
        Assert.notNull(query, "Query must not be null!");
        Assert.notNull(entityClass, "EntityClass must not be null!");
        Assert.notNull(collectionName, "CollectionName must not be null!");
        query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));

        return super.findOne(query, entityClass, collectionName);
    }

    @Override
    @SuppressWarnings("ConstantConditions")
    public boolean exists(Query query, @Nullable Class<?> entityClass, String collectionName) {
        if (query == null) {
            throw new InvalidDataAccessApiUsageException("Query passed in to exist can't be null");
        }

        query.addCriteria(Criteria.where("deletedAt").exists(Boolean.FALSE));

        return super.exists(query, entityClass, collectionName);
    }

// You can also override ```delete()``` method, but I decided to not to do this

//  Maybe here should add other methods: count, findAndModify and ect. It depends which methods you going to use.
}
  1. Then create Bean in configuration class:
@Configuration
public class MyConfiguration {
//...
    @Bean(name = "mongoTemplate")
    CustomMongoTemplate customMongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
        return new CustomMongoTemplate(databaseFactory, converter);
    }
//...
}
  1. And allow Spring to override default MongoTemplate bean. Add next thing to your application.properties file:
spring.main.allow-bean-definition-overriding=true
  1. Replace delete() with set deletedAt:
// Deletion method

// Before
User = userRepository.findById(id);
userRepository.delete(user);

// Now
User = userRepository.findById(id);
user.setDeletedAt(LocalDateTime.now()); 
userRepository.save(user);



Second Step. Implement soft delete for method in Repositories (generated by JPA) like as findAllByEmail(String email), existsByNameAndUsername(String name, String username)... .

Resource: https://blog.rpuch.com/2019/10/27/spring-data-mongo-soft-delete-repositories.html

  1. SoftDeleteMongoQueryLookupStrategy
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.lang.reflect.Method;

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

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

    @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().andOperator(
                Criteria.where("deletedAt").exists(false)
        );
    }

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

        @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());
        }
    }
}
  1. SeesSoftlyDeletedRecords (if you marked method annotation then method will ignore soft-deletion)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SeesSoftlyDeletedRecords {
}
  1. SoftDeleteMongoRepositoryFactory
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;

import java.util.Optional;

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 Optional.of(createSoftDeleteQueryLookupStrategy(optStrategy.get(), evaluationContextProvider));
    }

    private SoftDeleteMongoQueryLookupStrategy createSoftDeleteQueryLookupStrategy(QueryLookupStrategy strategy,
                                                                                   QueryMethodEvaluationContextProvider evaluationContextProvider) {
        return new SoftDeleteMongoQueryLookupStrategy(strategy, mongoOperations, evaluationContextProvider);
    }
}
  1. SoftDeleteMongoRepositoryFactoryBean
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import java.io.Serializable;

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);
    }
}
  1. Add it to configuration
@Configuration
@EnableMongoRepositories(basePackages = {"path to package with your repositories"}, repositoryFactoryBeanClass = SoftDeleteMongoRepositoryFactoryBean.class)
public class MyConfiguration {
//...
}

Hope it will help someone)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM