思考:
- MongnDB 的插入和更新操作是否可以做增强呢?
- MongnDB 的 mapper 继承 MongoRepository,MongoRepository 拥有大量常用接口,但这些接口好用吗,不符合需求的话,我们是否可以重写呢?
- MongnDB 因为联表查询较少,日常查询可用 JPA 实现,那么可以对 JPA 查询进行增强吗?
对应场景:
- 统一插入 id、添加创建人、创建时间等
- 物理删除改为逻辑删除、添加默认查询条件等
- 添加默认查询条件
插入和更新
AbstractMappingEventListener 中存在以下回调方法:
- onBeforeConvert:在 object 被 MongoConverte r转换为 Document 之前,在 MongoTemplate insert,insertLis t和 save 操作中调用。
- onBeforeSave: 在将 Document 插入或保存在数据库中之前,请在 MongoTemplate insert,insertList 和 save 操作中调用。
- onAfterSave: 在将 Document 插入或保存在数据库中之后,在 MongoTemplate insert,insertList 和 save 操作中调用。
- onAfterLoad: 从数据库中检索 Document 后,在 MongoTemplate find,findAndRemove,findOne,findAll 和 getCollection 方法中调用。
- onAfterConvert: 从数据库中检索到 Document 后,在 MongoTemplate find,findAndRemove,findOne,findAll 和 getCollection方法中调用被转换为 POJO
可以看出,这个监听器可以对插入和更新的传参进行过滤,也可以对查询结果进行过滤
下面举例说明如何自定义 id 生成策略
@Configuration
public class CommonFieldsEventListener extends AbstractMongoEventListener {
@Override
public void onBeforeConvert(BeforeConvertEvent event) {
Object source = event.getSource();
if (source instanceof BasePO) {
LocalDateTime now = LocalDateTime.now();
BasePO entity = BasePO.class.cast(source);
/* 使用随机数做 id */
if(entity.getId() == null){
entity.setId(RandomUtils.nextLong());
}
}
}
}
BasePO 为公共实体,每个数据库表实体都必须继承它
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class BasePO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
@Id
private Long id;
...
}
MongoRepository
继承 MongoRepository
@NoRepositoryBean:使用了该注解的接口不会被单独创建实例,只会作为其他接口的父接口而被使用。
@NoRepositoryBean
public interface CusMongoRepository<T, ID> extends MongoRepository<T, ID> {
...
}
实现类
public class CusMongoRepositoryImpl<T, ID> implements CusMongoRepository<T, ID> {
...
}
操作 Mapper 继承自定义的 CusMongoRepository
@Repository
public interface TestMapper extends CusMongoRepository<TestPO, Long> {
}
然而启动后会报错
Error creating bean with name 'testMapper' defined in com.njc.mongodb.mapper.TestMapper defined in @EnableMongoRepositories declared on MongoRepositoriesRegistrar.EnableMongoRepositoriesConfiguration: Invocation of init method failed;
nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract com.mongodb.client.result.UpdateResult com.njc.mongodb.config.CusMongoRepository.updateMulti
原因:自定义类 CusMongoRepositoryImpl 并没有实例化,
方案:从报错可以看出,通过 @EnableMongoRepositories 配置 使用解释
repositoryBaseClass:Configure the repository base class to be used to create repository proxies for this particular configuration.
意思是:配置用于为此特定配置创建存储库代理的存储库基类
启动类装配
@SpringBootApplication
@EnableMongoRepositories(repositoryBaseClass = CusMongoRepositoryImpl.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application .class, args);
}
}
JPA
假设有个需求,需要根据名称查询数据,使用 JPA 可以快速实现 findByName,下面以这个需求为例子做探讨
@Repository
public interface TestMapper extends CusMongoRepository<TestPO, Long> {
TestPO findByName(String name);
}
寻找增强点
- MongoRepositoryFactoryBean 创建 MongoRepositoryFactory
public class MongoRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends RepositoryFactoryBeanSupport<T, S, ID> {
...
protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
return new MongoRepositoryFactory(operations);
}
}
- MongoRepositoryFactory 创建 PartTreeMongoQuery
public class MongoRepositoryFactory extends RepositoryFactorySupport {
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, mappingContext);
queryMethod.verify();
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionParser,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedAggregation()) {
return new StringBasedAggregation(queryMethod, operations, expressionParser, evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
} else {
return new PartTreeMongoQuery(queryMethod, operations, expressionParser, evaluationContextProvider);
}
}
}
}
- PartTreeMongoQuery 的 createQuery 方法,为 JPA 创建查询条件的方法
public class PartTreeMongoQuery extends AbstractMongoQuery {
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
Query query = creator.createQuery();
}
}
- PartTree 是 PartTreeMongoQuery 的一个入参,从此可以看出 findByName 和 queryByName 可以打到同样效果
public class PartTree implements Streamable<OrPart> {
private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
private static final String COUNT_PATTERN = "count";
private static final String EXISTS_PATTERN = "exists";
private static final String DELETE_PATTERN = "delete|remove";
private static final Pattern PREFIX_TEMPLATE = Pattern.compile( //
"^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + EXISTS_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");
...
}
配置
自定义 MongoQuery 继承 PartTreeMongoQuery,举例场景:添加条件,删除标识等于 N
public class CusPartTreeMongoQuery extends PartTreeMongoQuery {
public CusPartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, mongoOperations, expressionParser, evaluationContextProvider);
}
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
Query query = super.createQuery(accessor);
query.addCriteria(Criteria.where("deleteFlag").is("N"));
return query;
}
}
自定义 CusMongoRepositoryFactory 继承 MongoRepositoryFactory
- getQueryLookupStrategy:创建自定义的工厂 CusMongoRepositoryFactory
- resolveQuery:返回自定义的查询策略 CusPartTreeMongoQuery
public class CusMongoRepositoryFactory extends MongoRepositoryFactory {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final MongoOperations operations;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
public CusMongoRepositoryFactory(MongoOperations mongoOperations) {
super(mongoOperations);
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
this.operations = mongoOperations;
this.mappingContext = mongoOperations.getConverter().getMappingContext();
}
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new CusMongoRepositoryFactory.MongoQueryLookupStrategy(operations, evaluationContextProvider, mappingContext));
}
private static class MongoQueryLookupStrategy implements QueryLookupStrategy {
private final MongoOperations operations;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
public MongoQueryLookupStrategy(MongoOperations operations,
QueryMethodEvaluationContextProvider evaluationContextProvider,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.operations = operations;
this.evaluationContextProvider = evaluationContextProvider;
this.mappingContext = mappingContext;
}
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, mappingContext);
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringBasedMongoQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedAggregation()) {
return new StringBasedAggregation(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
} else {
return new CusPartTreeMongoQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider);
}
}
}
}
创建自定义 MongoRepositoryFactoryBean,用来创建自定义 CusMongoRepositoryFactory
public class CusMongoRepositoryFactoryBean <T extends Repository<S, ID>, S, ID extends Serializable> extends MongoRepositoryFactoryBean<T, S, ID> {
/**
* Creates a new {@link MongoRepositoryFactoryBean} for the given repository interface.
*
* @param repositoryInterface must not be {@literal null}.
*/
public CusMongoRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}
/**
* Creates and initializes a {@link RepositoryFactorySupport} instance.
*
* @param operations
* @return
*/
@Override
protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) {
return new CusMongoRepositoryFactory(operations);
}
}
此时 FactoryBean、Factory、Query 都已实现了自定义,然后重启后断点发现,并没有走到我们自定义 Query 里面去
原因是 FactoryBean 并没有被 spring 实例化
方案:通过 @EnableMongoRepositories 配置 使用解释
repositoryFactoryBeanClass:Returns the FactoryBean class to be used for each repository instance.
意思是:返回要用于每个存储库实例的 FactoryBean 类
启动类装配
@SpringBootApplication
@EnableMongoRepositories(repositoryFactoryBeanClass= CusMongoRepositoryFactoryBean.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application .class, args);
}
}