一、背景
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 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)
關鍵信息: 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://www.cnblogs.com/littleatp/p/10043447.html