
一、背景
spring-data-mongo 实现了基于 MongoDB 的 ORM-Mapping 能力,
通过一些简单的注解、Query 封装以及工具类,就可以通过对象操作来实现集合、文档的增删改查;
在 SpringBoot 体系中,spring-data-mongo 是 MongoDB Java 工具库的不二之选。
二、问题产生
在一次项目问题的追踪中,发现 SpringBoot 应用启动失败,报错信息如下:
Error creating bean with name 'mongoTemplate' defined in class path resource [org/bootfoo/BootConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.mongodb.core.MongoTemplate]: Factory method 'mongoTemplate' threw exception; nested exception is org.springframework.dao.DataIntegrityViolationException: Cannot create index for 'deviceId' in collection 'T_MDevice' with keys '{ "deviceId" : 1}' and options '{ "name" : "deviceId"}'. Index already defined as '{ "v" : 1 , "unique" : true , "key" : { "deviceId" : 1} , "name" : "deviceId" , "ns" : "appdb.T_MDevice"}'.; nested exception is com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 } at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553) ... Caused by: org.springframework.dao.DataIntegrityViolationException: Cannot create index for 'deviceId' in collection 'T_MDevice' with keys '{ "deviceId" : 1}' and options '{ "name" : "deviceId"}'. Index already defined as '{ "v" : 1 , "unique" : true , "key" : { "deviceId" : 1} , "name" : "deviceId" , "ns" : "appdb.T_MDevice"}'.; nested exception is com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 } at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.createIndex(MongoPersistentEntityIndexCreator.java:157) at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.checkForAndCreateIndexes(MongoPersistentEntityIndexCreator.java:133) at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.checkForIndexes(MongoPersistentEntityIndexCreator.java:125) at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.<init>(MongoPersistentEntityIndexCreator.java:91) at org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator.<init>(MongoPersistentEntityIndexCreator.java:68) at org.springframework.data.mongodb.core.MongoTemplate.<init>(MongoTemplate.java:229) at org.bootfoo.BootConfiguration.mongoTemplate(BootConfiguration.java:121) at org.bootfoo.BootConfiguration$$EnhancerBySpringCGLIB$$1963a75.CGLIB$mongoTemplate$2(<generated>) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162) ... 58 more Caused by: com.mongodb.MongoCommandException: Command failed with error 85: 'exception: Index with name: deviceId already exists with different options' on server 127.0.0.1:27017. The full response is { "createdCollectionAutomatically" : false, "numIndexesBefore" : 6, "errmsg" : "exception: Index with name: deviceId already exists with different options", "code" : 85, "ok" : 0.0 } at com.mongodb.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:115) at com.mongodb.connection.CommandProtocol.execute(CommandProtocol.java:114) at com.mongodb.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:168)
关键信息:org.springframework.dao.DataIntegrityViolationException: Cannot create index
从异常信息上看,出现的是索引冲突(Command failed with error 85),spring-data-mongo 组件在程序启动时会实现根据注解创建索引的功能。
查看业务实体定义:
@Document(collection = "T_MDevice")public class MDevice { @Id private String id; @Indexed(unique=true) private String deviceId;
deviceId 这个字段上定义了一个索引,unique=true 表示这是一个唯一索引。
我们继续 查看 MongoDB 中表的定义:
db.getCollection('T_MDevice').getIndexes() >>[ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "appdb.T_MDevice" }, { "v" : 1, "key" : { "deviceId" : 1 }, "name" : "deviceId", "ns" : "appdb.T_MDevice" }]
发现数据库表中同样存在一个名为 deviceId 的索引,但是并非唯一索引!
三、详细分析
为了核实错误产生的原因,我们尝试通过 Mongo Shell 去执行索引的创建,发现返回了同样的错误。
通过将数据库中的索引删除,或更正为 unique=true 之后可以解决当前的问题。
从严谨度上看,一个索引冲突导致 SpringBoot 服务启动不了,是可以接受的。
但从灵活性来看,是否有某些方式能禁用索引的自动创建,或者仅仅是打印日志呢?
尝试 google spring data mongodb disable index creation
发现 JIRA-DATAMONGO-1201 在 2015 年就已经提出,至今未解决。
图
stackoverflow 找到许多同样问题,
但大多数的解答是不采用索引注解,选择其他方式对索引进行管理。
这些结果并不能令人满意。
尝试查看 spring-data-mongo 的机制,定位到 MongoPersistentEntityIndexCreator 类:
初始化方法中,会根据 MappingContext(实体映射上下文)中已有的实体去创建索引
public MongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory, IndexResolver indexResolver) { ... //根据已有实体创建 for (MongoPersistentEntity<?> entity : mappingContext.getPersistentEntities()) { checkForIndexes(entity); } }
在接收到MappingContextEvent时,创建对应实体的索引
public void onApplicationEvent(MappingContextEvent<?, ?> event) { if (!event.wasEmittedBy(mappingContext)) { return; } PersistentEntity<?, ?> entity = event.getPersistentEntity(); // Double check type as Spring infrastructure does not consider nested generics if (entity instanceof MongoPersistentEntity) { //创建单个实体索引 checkForIndexes((MongoPersistentEntity<?>) entity); } }
MongoPersistentEntityIndexCreator 是通过 MongoTemplate 引入的,如下:
public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverter) { Assert.notNull(mongoDbFactory); this.mongoDbFactory = mongoDbFactory; this.exceptionTranslator = mongoDbFactory.getExceptionTranslator(); this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter; ... // We always have a mapping context in the converter, whether it's a simple one or not mappingContext = this.mongoConverter.getMappingContext(); // We create indexes based on mapping events if (null != mappingContext && mappingContext instanceof MongoMappingContext) { indexCreator = new MongoPersistentEntityIndexCreator((MongoMappingContext) mappingContext, mongoDbFactory); eventPublisher = new MongoMappingEventPublisher(indexCreator); if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } } } ... //MongoTemplate实现了 ApplicationContextAware,当ApplicationContext被实例化时被感知 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { prepareIndexCreator(applicationContext); eventPublisher = applicationContext; if (mappingContext instanceof ApplicationEventPublisherAware) { //MappingContext作为事件来源,向ApplicationContext发布 ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } resourceLoader = applicationContext; } ... //注入事件监听 private void prepareIndexCreator(ApplicationContext context) { String[] indexCreators = context.getBeanNamesForType(MongoPersistentEntityIndexCreator.class); for (String creator : indexCreators) { MongoPersistentEntityIndexCreator creatorBean = context.getBean(creator, MongoPersistentEntityIndexCreator.class); if (creatorBean.isIndexCreatorFor(mappingContext)) { return; } } if (context instanceof ConfigurableApplicationContext) { //使 IndexCreator 监听 ApplicationContext的事件 ((ConfigurableApplicationContext) context).addApplicationListener(indexCreator); } }
MongoPersistentEntityIndexCreator 是通过 MongoTemplate 引入的,如下:
public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverter) { Assert.notNull(mongoDbFactory); this.mongoDbFactory = mongoDbFactory; this.exceptionTranslator = mongoDbFactory.getExceptionTranslator(); this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter; ... // We always have a mapping context in the converter, whether it's a simple one or not mappingContext = this.mongoConverter.getMappingContext(); // We create indexes based on mapping events if (null != mappingContext && mappingContext instanceof MongoMappingContext) { indexCreator = new MongoPersistentEntityIndexCreator((MongoMappingContext) mappingContext, mongoDbFactory); eventPublisher = new MongoMappingEventPublisher(indexCreator); if (mappingContext instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } } } ... //MongoTemplate实现了 ApplicationContextAware,当ApplicationContext被实例化时被感知 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { prepareIndexCreator(applicationContext); eventPublisher = applicationContext; if (mappingContext instanceof ApplicationEventPublisherAware) { //MappingContext作为事件来源,向ApplicationContext发布 ((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher); } resourceLoader = applicationContext; } ... //注入事件监听 private void prepareIndexCreator(ApplicationContext context) { String[] indexCreators = context.getBeanNamesForType(MongoPersistentEntityIndexCreator.class); for (String creator : indexCreators) { MongoPersistentEntityIndexCreator creatorBean = context.getBean(creator, MongoPersistentEntityIndexCreator.class); if (creatorBean.isIndexCreatorFor(mappingContext)) { return; } } if (context instanceof ConfigurableApplicationContext) { //使 IndexCreator 监听 ApplicationContext的事件 ((ConfigurableApplicationContext) context).addApplicationListener(indexCreator); } }
由此可见,MongoTemplate 在初始化时,先通过 MongoConverter 带入 MongoMappingContext,
随后完成一系列初始化,整个过程如下:
实例化 MongoTemplate;
实例化 MongoConverter;
实例化 MongoPersistentEntityIndexCreator;
初始化索引(通过 MappingContext 已有实体);
Repository 初始化 -> MappingContext 发布映射事件;
ApplicationContext 将事件通知到 IndexCreator;
IndexCreator 创建索引
在实例化过程中,没有任何配置可以阻止索引的创建。
四、解决问题
从前面的分析中,可以发现问题关键在 IndexCreator,能否提供一个自定义的实现呢,答案是可以的!
实现的要点如下
实现一个 IndexCreator,可继承 MongoPersistentEntityIndexCreator,去掉索引的创建功能;
实例化 MongoConverter 和 MongoTemplate 时,使用一个空的 MongoMappingContext 对象避免初始化索引;
将自定义的 IndexCreator 作为 Bean 进行注册,这样在 prepareIndexCreator 方法执行时,
原来的 MongoPersistentEntityIndexCreator 不会监听 ApplicationContext 的事件
IndexCreator 实现了 ApplicationContext 监听,接管 MappingEvent 事件处理。
实例化 Bean
@Bean public MongoMappingContext mappingContext() { return new MongoMappingContext(); } // 使用 MappingContext 实例化 MongoTemplate @Bean public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext mappingContext) { MappingMongoConverter converter = new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory), mappingContext); converter.setTypeMapper(new DefaultMongoTypeMapper(null)); MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter); return mongoTemplate; }
自定义 IndexCreator
// 自定义IndexCreator实现 @Component public static class CustomIndexCreator extends MongoPersistentEntityIndexCreator { // 构造器引用MappingContext public CustomIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory) { super(mappingContext, mongoDbFactory); } public void onApplicationEvent(MappingContextEvent<?, ?> event) { PersistentEntity<?, ?> entity = event.getPersistentEntity(); // 获得Mongo实体类 if (entity instanceof MongoPersistentEntity) { System.out.println("Detected MongoEntity " + entity.getName()); //可实现索引处理.. } } }
在这里 CustomIndexCreator 继承了 MongoPersistentEntityIndexCreator,将自动接管 MappingContextEvent 事件的监听。
在业务实现上可以根据需要完成索引的处理!
小结
spring-data-mongo 提供了非常大的便利性,但在灵活性支持上仍然不足。上述的方法实际上有些隐晦,在官方文档中并未提及这样的方式。
ORM-Mapping 框架在实现 Schema 映射处理时需要考虑校验级别,比如 Hibernate 便提供了 none/create/update/validation 多种选择,毕竟这对开发者来说更加友好。
期待 spring-data-mongo 在后续的演进中能尽快完善 Schema 的管理功能!
更多内容推荐
一个项目的 SpringCloud 微服务改造过程
本文介绍将一个老项目改造成SpringCloud架构,并且把前后端分离,前端采用Vue框架的经验。
Compass 2.0:简化、集成及性能提升
Compass是基于Lucene的一个开源项目,其主旨在于简化将搜索集成到Java应用的过程。最近,该项目发布了2.0版本。InfoQ就此采访了Compass的创始人Shay Banon以获得关于该版本更多更详尽的信息,并且对Compass为Java社区所做的贡献进行了一番深入的了解。
深入学习微框架:Spring Boot
在探讨面向Java微框架的系列文章中,Dan Woods第一篇介绍了Spring Boot。
Spring Bean 的别名:为什么命名 Bean 还需要别名?
2020 年 1 月 16 日
"application" Bean 作用域:application Bean 是否真的有必要?
2020 年 3 月 5 日
MongoDB 索引机制(二)
2020 年 1 月 15 日
在 Kubernetes 集群上部署和管理 JFrog Artifactory
本文来自RancherLabs微信公众号
这竟是索引的“锅”!
最近在开发异地交易可视化项目中,为了做不同城市系统的兼容,需要获取当前所属的城市编码来区分不同appId,利用框架查询表apps结果,与偶然自己手写sql查询表数据对比,发现两次查询结果竟然不一致。
PV、PVC、StorageClass,这些到底在说啥?
PVC描述的,是Pod想要使用的持久化存储的属性;PV描述的,则是一个具体的Volume的属性;而StorageClass描述的,则是PV和PVC的绑定关系。
2018 年 10 月 26 日
CouchDB 是什么?为什么我们要关注它?
CouchDB是一个NoSQL解决方案,是一个面向文档的数据库,在它里面所有文档域(Field)都是以键值对的形式存储的。CouchDB有一些独特的特性,例如高级复制。本文将介绍入门知识、单元测试、CRUD以及查询操作。
MongoDB 安全加固实践
2020 年 1 月 15 日
Shardingsphere 整合 Narayana 对 XA 分布式事务的支持(4)
Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立,却又能够混合部署配合使用的产品组成。它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
Hades——JPA 的开源实现
几乎所有的应用系统都需要通过访问数据来完成工作。在领域驱动设计方法中,通过为实体类定义资源库来实现领域对象的持久化。Java开发者经常使用JPA来实现持久化。Hades是一个开源项目,基于JPA和Spring构建,通过简化开发、减少工作量改进数据访问层的实现。
你要的 Elasticsearch ORM 框架终于来了
如果你在使用Elasticsearch的过程中,还在为构建Elasticsearch的DSL语句而苦恼,还在为构建复杂冗长的条件而头疼,还在为一次次的响应提取而奔溃,那你这时候需要一个简单方便上手的Elasticsearch ORM框架:**ebatis**!
深入理解 Spring 异常处理
Spring开发中实现统一的异常处理分析。
NHibernate 和 Entity Framework 4.0 优劣势争论
最近,Oren Eini(也被称为Ayende Rahein)发表了一个帖子,从而引发了关于NHibernate和Entity Framework 4.0各自优点和功能的讨论,而这二者都是基于.NET的对象/关系映射框架。InfoQ对此讨论进行了深入的探究,以了解其中提到的观点。
如何在低版本 Spring 中快速实现类似自动配置的功能
2019 年 4 月 3 日
推荐阅读
116|后端:创建新增收货地址接口、添加索引
2020 年 10 月 29 日
将数据从 PostgreSQL 同步到 Elasticsearch 的经验总结
基于 Lucene 的分布式搜索引擎: Elasticsearch 1.3.0 发布
Spring Data —— 完全统一的 API?
JPA 2.2 带来一些备受期待的变更
引用的缺省值 null
2019 年 5 月 27 日
依赖管理(二):第三方组件库在 Flutter 中要如何管理?
2019 年 8 月 8 日
电子书

大厂实战PPT下载
换一换 
陈睿 | 腾讯 腾讯专家工程师
占利军 | 阿里巴巴 高级技术专家
金晓军 | 阿里巴巴 高级技术专家










评论