好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

SpringBoot MongoDB 索引冲突分析及解决方法

一、背景

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测试数据mandprotocol.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 组件在程序启动时会实现根据注解创建索引的功能。

查看业务实体定义:

?

1

2

3

4

5

6

7

8

@document (collection = "t_mdevice" )

public class mdevice {

 

   @id

   private string id;

 

   @indexed (unique= true )

   private string deviceid;

deviceid 这个字段上定义了一个索引, unique=true 表示这是一个唯一索引。

我们继续 查看 mongodb中表的定义:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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(实体映射上下文)中已有的实体去创建索引

?

1

2

3

4

5

6

7

8

public mongopersistententityindexcreator(mongomappingcontext mappingcontext, mongodbfactory mongodbfactory,

       indexresolver indexresolver) {

     ...

     //根据已有实体创建

     for (mongopersistententity<?> entity : mappingcontext.getpersistententities()) {

       checkforindexes(entity);

     }

   }

在接收到mappingcontextevent时,创建对应实体的索引

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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引入的,如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// 自定义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的管理功能!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

原文链接:http://HdhCmsTestcnblogs测试数据/littleatp/p/10043447.html

查看更多关于SpringBoot MongoDB 索引冲突分析及解决方法的详细内容...

  阅读:18次