SpringBoot MongoDB 索引沖突分析及解決方法
一、背景
spring-data-mongo 實現(xiàn)了基于 MongoDB 的 ORM-Mapping 能力,
通過一些簡單的注解、Query封裝以及工具類,就可以通過對象操作來實現(xiàn)集合、文檔的增刪改查;
在 SpringBoot 體系中,spring-data-mongo 是 MongoDB Java 工具庫的不二之選。
二、問題產(chǎn)生
在一次項目問題的追蹤中,發(fā)現(xiàn)SpringBoot 應(yīng)用啟動失敗,報錯信息如下:
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 moreCaused 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)
關(guān)鍵信息: org.springframework.dao.DataIntegrityViolationException: Cannot create index
從異常信息上看,出現(xiàn)的是索引沖突( Command failed with error 85 ),spring-data-mongo 組件在程序啟動時會實現(xiàn)根據(jù)注解創(chuàng)建索引的功能。
查看業(yè)務(wù)實體定義:
@Document(collection = "T_MDevice")
public class MDevice {
@Id
private String id;
@Indexed(unique=true)
private String deviceId;
deviceId 這個字段上定義了一個索引, unique=true 表示這是一個唯一索引。
我們繼續(xù) 查看 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"
}
]
發(fā)現(xiàn)數(shù)據(jù)庫表中同樣存在一個名為 deviceId的索引,但是并非唯一索引!
三、詳細(xì)分析
為了核實錯誤產(chǎn)生的原因,我們嘗試通過 Mongo Shell去執(zhí)行索引的創(chuàng)建,發(fā)現(xiàn)返回了同樣的錯誤。
通過將數(shù)據(jù)庫中的索引刪除,或更正為 unique=true 之后可以解決當(dāng)前的問題。
從嚴(yán)謹(jǐn)度上看,一個索引沖突導(dǎo)致 SpringBoot 服務(wù)啟動不了,是可以接受的。
但從靈活性來看,是否有某些方式能 禁用索引的自動創(chuàng)建 ,或者僅僅是打印日志呢?
嘗試 google spring data mongodb disable index creation
發(fā)現(xiàn) JIRA-DATAMONGO-1201 在2015年就已經(jīng)提出,至今未解決。

圖
stackoverflow 找到許多 同樣問題 ,
但大多數(shù)的解答是不采用索引注解,選擇其他方式對索引進(jìn)行管理。
這些結(jié)果并不能令人滿意。
嘗試查看 spring-data-mongo 的機制,定位到 MongoPersistentEntityIndexCreator 類:
初始化方法中,會根據(jù) MappingContext(實體映射上下文)中已有的實體去創(chuàng)建索引
public MongoPersistentEntityIndexCreator(MongoMappingContext mappingContext, MongoDbFactory mongoDbFactory,
IndexResolver indexResolver) {
...
//根據(jù)已有實體創(chuàng)建
for (MongoPersistentEntity<?> entity : mappingContext.getPersistentEntities()) {
checkForIndexes(entity);
}
}
在接收到MappingContextEvent時,創(chuàng)建對應(yīng)實體的索引
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) {
//創(chuàng)建單個實體索引
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實現(xiàn)了 ApplicationContextAware,當(dāng)ApplicationContext被實例化時被感知
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
prepareIndexCreator(applicationContext);
eventPublisher = applicationContext;
if (mappingContext instanceof ApplicationEventPublisherAware) {
//MappingContext作為事件來源,向ApplicationContext發(fā)布
((ApplicationEventPublisherAware) mappingContext).setApplicationEventPublisher(eventPublisher);
}
resourceLoader = applicationContext;
}
...
//注入事件監(jiān)聽
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 監(jiān)聽 ApplicationContext的事件
((ConfigurableApplicationContext) context).addApplicationListener(indexCreator);
}
}
由此可見, MongoTemplate 在初始化時,先通過 MongoConverter 帶入 MongoMappingContext,
隨后完成一系列初始化,整個過程如下:
- 實例化 MongoTemplate;
- 實例化 MongoConverter;
- 實例化 MongoPersistentEntityIndexCreator;
- 初始化索引(通過MappingContext已有實體);
- Repository初始化 -> MappingContext 發(fā)布映射事件;
- ApplicationContext 將事件通知到 IndexCreator;
- IndexCreator 創(chuàng)建索引
在實例化過程中,沒有任何配置可以阻止索引的創(chuàng)建。
四、解決問題
從前面的分析中,可以發(fā)現(xiàn)問題關(guān)鍵在 IndexCreator,能否提供一個自定義的實現(xiàn)呢,答案是可以的!
實現(xiàn)的要點如下
- 實現(xiàn)一個IndexCreator,可繼承MongoPersistentEntityIndexCreator,去掉索引的創(chuàng)建功能;
- 實例化 MongoConverter和 MongoTemplate時,使用一個空的 MongoMappingContext對象避免初始化索引;
- 將自定義的IndexCreator作為Bean進(jìn)行注冊,這樣在prepareIndexCreator方法執(zhí)行時,原來的 MongoPersistentEntityIndexCreator不會監(jiān)聽ApplicationContext的事件
- IndexCreator 實現(xiàn)了ApplicationContext監(jiān)聽,接管 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實現(xiàn)
@Component
public static class CustomIndexCreator extends MongoPersistentEntityIndexCreator {
// 構(gòu)造器引用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());
//可實現(xiàn)索引處理..
}
}
}
在這里 CustomIndexCreator繼承了 MongoPersistentEntityIndexCreator ,將自動接管MappingContextEvent事件的監(jiān)聽。
在業(yè)務(wù)實現(xiàn)上可以根據(jù)需要完成索引的處理!
小結(jié)
spring-data-mongo 提供了非常大的便利性,但在靈活性支持上仍然不足。上述的方法實際上有些隱晦,在官方文檔中并未提及這樣的方式。
ORM-Mapping 框架在實現(xiàn)Schema映射處理時需要考慮校驗級別,比如 Hibernate便提供了 none/create/update/validation 多種選擇,畢竟這對開發(fā)者來說更加友好。
期待 spring-data-mongo 在后續(xù)的演進(jìn)中能盡快完善 Schema的管理功能!
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
javaweb啟動時啟動socket服務(wù)端代碼實現(xiàn)
這篇文章主要介紹了javaweb啟動時啟動socket服務(wù)端代碼實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
SpringBoot復(fù)雜參數(shù)應(yīng)用詳細(xì)講解
我們在編寫接口時會傳入復(fù)雜參數(shù),如Map、Model等,這種類似的參數(shù)會有相應(yīng)的參數(shù)解析器進(jìn)行解析,并且最后會將解析出的值放到request域中,下面我們一起來探析一下其中的原理2022-09-09
SpringBoot如何使用MyBatis-Plus實現(xiàn)高效的數(shù)據(jù)訪問層
在開發(fā) Spring Boot 應(yīng)用時,數(shù)據(jù)訪問是不可或缺的部分,本文將詳細(xì)介紹如何在 Spring Boot 中使用 MyBatis-Plus,并結(jié)合具體代碼示例來講解它的使用方法和常見配置,希望對大家有一定的幫助2025-04-04
Spring Security+JWT實現(xiàn)認(rèn)證與授權(quán)的實現(xiàn)
本文主要介紹了Spring Security+JWT實現(xiàn)認(rèn)證與授權(quán)的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Java設(shè)計模式之外觀模式(Facade模式)介紹
這篇文章主要介紹了Java設(shè)計模式之外觀模式(Facade模式)介紹,外觀模式(Facade)的定義:為子系統(tǒng)中的一組接口提供一個一致的界面,需要的朋友可以參考下2015-03-03
Spring通過<import>標(biāo)簽導(dǎo)入外部配置文件
之前文章里我們講到Spring加載Xml配置文件的細(xì)節(jié),那么加載完了我們肯定要解析這個配置文件中定義的元素。這篇我們首先來分析下Spring是如何通過標(biāo)簽導(dǎo)入外部配置文件的。2021-06-06

