MongoTemplate配置
spring:
data:
mongodb:
uri: mongodb://root:password@ip1:27000,ip2:27000/test
一般情況下柑司,按照如下配置糟袁,springboot會(huì)進(jìn)行自動(dòng)裝配乱豆,但是如果需要實(shí)現(xiàn)一些自定義的功能辜窑,例如密碼加解密钩述,類型轉(zhuǎn)換等功能需要手寫(xiě)配置MongoTemplate。
@Configuration
@EnableMongoRepositories()
public class MongoTemplateConfig {
@Autowired
TimestampConverter timestampConverter;
private final String URI_PATTERN = "(mongodb.*:)(.*?)(@.+)";
@Bean
public MongoDatabaseFactory mongoDbFactory(MongoProperties properties) throws Exception {
final boolean match = ReUtil.isMatch(URI_PATTERN, properties.getUri());
final String newUri;
if (match) {
String password = ReUtil.extractMulti(URI_PATTERN, properties.getUri(), "$2");
final String passwordDecrypt = StringUtils.reverse(password);
newUri = StringUtils.replace(properties.getUri(), password, passwordDecrypt);
} else {
throw new BusiException(SystemFlag.BUSINESS_ERROR,"the Uri of mongodb parsed error");
}
ConnectionString connectionString = new ConnectionString(newUri);
return new SimpleMongoClientDatabaseFactory(connectionString);
}
@Bean
public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory factory,
MongoMappingContext context, BeanFactory beanFactory,
@Qualifier("mongoCusConversions") CustomConversions conversions) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver,
context);
mappingConverter.setCustomConversions(conversions);
//不保存_class
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
return mappingConverter;
}
@Bean(name = "mongoCusConversions")
@Primary
public CustomConversions mongoCustomConversions() {
return new MongoCustomConversions(Arrays.asList(timestampConverter));
}
@Bean
@Primary
public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory,
MongoConverter converter) throws UnknownHostException {
return new MongoTemplate(mongoDbFactory, converter);
}
}
@EnableMongoRepositories()表示支持Spring JPA穆碎,即通過(guò)規(guī)范命名的接口來(lái)實(shí)現(xiàn)簡(jiǎn)單的DB操作牙勘,不需要自己寫(xiě)Query,可以通過(guò)該注解的value屬性來(lái)指定注解的作用范圍所禀。
ReUtil是一個(gè)正則表達(dá)式的工具類方面,用于判斷配置文件的格式是否正確,配置MongoDatabaseFactory過(guò)程中實(shí)現(xiàn)一個(gè)比較簡(jiǎn)單的配置文件解密的過(guò)程色徘,解密方法用簡(jiǎn)單的字符串翻轉(zhuǎn)來(lái)實(shí)現(xiàn)恭金。
通過(guò)MappingMongoConverter來(lái)實(shí)現(xiàn)java中的對(duì)象與MongoDB中的Document進(jìn)行一些復(fù)雜的映射,默認(rèn)情況下一個(gè)java域?qū)ο蟠嫒隡ongoDB時(shí)會(huì)生成一個(gè)"_class"的key對(duì)應(yīng)存儲(chǔ)Java對(duì)象類型褂策,通過(guò)
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
來(lái)取消每條記錄生成一個(gè)"-class"的數(shù)據(jù)横腿。
通過(guò)MappingMongoConverter實(shí)現(xiàn)一個(gè)簡(jiǎn)單的時(shí)間轉(zhuǎn)化功能TimestampConverter,如下所示
@Component
public class TimestampConverter implements Converter<Date, Timestamp> {
@Override
public Timestamp convert(Date date) {
if (date != null) {
return new Timestamp(date.getTime());
}
return null;
}
}
還可以進(jìn)行更加精細(xì)化的配置辙培,例如
@WritingConverter
public class DateToString implements Converter<LocalDateTime, String> {
@Override
public String convert(LocalDateTime source) {
return source.toString() + 'Z';
}
}
// Direction: MongoDB -> Java
@ReadingConverter
public class StringToDate implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(source,DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
}
}
可以通過(guò)WritingConverter和ReadingConverter配置Document和Java對(duì)象相互轉(zhuǎn)化蔑水。
MongoTemplate實(shí)戰(zhàn)
例如一個(gè)博客系統(tǒng),我們通過(guò)MongoDB存儲(chǔ)用戶的瀏覽記錄扬蕊,瀏覽記錄的實(shí)體如下所示搀别,
@Document(collection = "visit_log")
@NoArgsConstructor
@Data
@Builder
@AllArgsConstructor
public class VisitLogEntity implements Serializable {
private static final long serialVersionUID = 683811304989731202L;
@Id
@Field("_id")
private String id;
@Field("page_id")
private String pageId;
@Field("viewer_name")
private String viewerName;
@Field("create_date")
private Timestamp createDate;
@Field("last_update_date")
private Timestamp lastUpdateDate;
@Builder.Default
private long viewCount = 1L;
}
如上所示,每個(gè)人對(duì)應(yīng)每篇文章有一條瀏覽記錄尾抑,每次訪問(wèn)都會(huì)對(duì)訪問(wèn)次數(shù)viewCount進(jìn)行+1操作.下文針對(duì)這個(gè)場(chǎng)景介紹MongoTemplate的基本操作歇父。
- findOne,根據(jù)查詢條件獲取一個(gè)結(jié)果再愈,返回第一個(gè)匹配的結(jié)果膳凝;
- exists刺洒,判斷符合查詢條件的記錄是否存在;
- find,獲取符合查詢結(jié)果的記錄独泞;
- findAndRemove,查詢符合條件的記錄并刪除亡笑。
這些操作用法基本一樣脸狸,如下所示,傳入一個(gè)封裝查詢條件的對(duì)象Query缴渊,Java中映射的對(duì)象entityClass和MongoDB中對(duì)應(yīng)的Document的名稱赏壹。
public <T> T findOne(Query query, Class<T> entityClass, String collectionName);
例如我們想要查詢某個(gè)用戶某篇博客的訪問(wèn)次數(shù),我們只需要通過(guò)博客id和訪問(wèn)者構(gòu)建查詢條件進(jìn)行查詢即可衔沼。
public List<VisitLogEntity> queryVisitLog(String pageId, String viewerName) {
Query query = new Query().addCriteria(Criteria.where("pageId").is(pageId).add("viewName").is(viewerName));
return mongoTemplate.find(query, VisitLogEntity.class);
}
findAndModify表示更新符合查詢條件的記錄蝌借,其方法如下所示昔瞧,
public <T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName);
}
Query封裝查詢條件,Update封裝的是更新內(nèi)容菩佑。例如用戶每次刷新頁(yè)面瀏覽次數(shù)會(huì)+1操作自晰,我們可以使用findAndModify操作,如下所示
public VisitLogEntity updateAndGet(VisitLogEntity visitLogEntity) {
Query query = new Query().addCriteria(Criteria.where("viewerName")
.is(visitLogEntity.getViewerName())
.and("pageId").is(visitLogEntity.getPageId())`);
boolean isExist = mongoTemplate.exists(query, VisitLogEntity.class);
if (isExist) {
Update update = new Update();
update.inc("viewCount", 1);
update.set("lastUpdateDate", new Timestamp(System.currentTimeMillis()));
return mongoTemplate.findAndModify(query, update, new FindAndModifyOptions().returnNew(true), VisitLogEntity.class);
} else {
visitLogEntity.setCreateDate(new Timestamp(System.currentTimeMillis()));
visitLogEntity.setLastUpdateDate(new Timestamp((System.currentTimeMillis())));
mongoTemplate.save(visitLogEntity);
return visitLogEntity;
}
}
如上所示擎鸠,首先判斷用戶是否存在訪問(wèn)記錄缀磕,如果存在則通過(guò)Update對(duì)訪問(wèn)次數(shù)viewCount進(jìn)行+1操作,若不存在訪問(wèn)記錄則新建訪問(wèn)記錄劣光。
保存操作包括主要包括insert和save方法袜蚕,這兩個(gè)方法都沒(méi)有返回值,同時(shí)兩個(gè)方法有一些區(qū)別绢涡,
- 單個(gè)記錄插入時(shí)牲剃,如果新數(shù)據(jù)的主鍵已經(jīng)存在,insert方法會(huì)報(bào)錯(cuò)DuplicateKeyException提示主鍵重復(fù)雄可,不保存當(dāng)前數(shù)據(jù)凿傅,而save方法會(huì)根據(jù)當(dāng)前數(shù)據(jù)對(duì)存量數(shù)據(jù)進(jìn)行更新;
- 進(jìn)行批量保存時(shí)数苫,insert方法可以一次性插入一個(gè)列表聪舒,不需要遍歷,而save方法需要遍歷列表進(jìn)行一個(gè)一個(gè)的插入虐急,insert方法的效率要高很多箱残。
該方法如下所示,
/**
* Performs an upsert. If no document is found that matches the query, a new document is created and inserted by
* combining the query document and the update document.
*
* @param query the query document that specifies the criteria used to select a record to be upserted
* @param update the update document that contains the updated object or $ operators to manipulate the existing object
* @param entityClass class of the pojo to be operated on
* @param collectionName name of the collection to update the object in
* @return the WriteResult which lets you access the results of the previous write.
*/
WriteResult upsert(Query query, Update update, Class<?> entityClass, String collectionName);
注釋說(shuō)明該方法的功能是止吁,如果存在與查詢條件匹配的文檔被辑,則根據(jù)Update中的內(nèi)容進(jìn)行更新,如果不存在符合查詢條件的內(nèi)容敬惦,則根據(jù)查詢條件和Update插入新的文檔盼理。
聚合查詢MongoDB 中聚合(aggregate)主要用于處理數(shù)據(jù)(諸如統(tǒng)計(jì)平均值,求和等)俄删,并返回計(jì)算后的數(shù)據(jù)結(jié)果宏怔。本文側(cè)重于Java實(shí)現(xiàn)。
結(jié)合上述中的訪問(wèn)記錄的場(chǎng)景畴椰,如果我們需要統(tǒng)計(jì)某個(gè)博主某個(gè)專欄下面所有文章的訪問(wèn)記錄臊诊,包括訪問(wèn)總?cè)藬?shù),訪問(wèn)總次數(shù)迅矛,以及每個(gè)訪客對(duì)應(yīng)的訪問(wèn)次數(shù)詳情,并且要滿足分頁(yè)需求潜叛,那么我們需要用到MongoDB的聚合操作秽褒,具體實(shí)現(xiàn)如下所示
public ResultEntity<VisitRecordEntity> queryVisitRecord(List<String> pageIds, long offset, int limit) {
Criteria criteria = Criteria.where("page_id").in(contentIds);
List<AggregationOperation> operations = new ArrayList<>();
operations.add(Aggregation.match(criteria));
operations.add(Aggregation.group("viewer_name").sum("view_count").as("viewCount").first("viewer_name").as("viewerName"));
//多線程處理提高響應(yīng)速度
CountDownLatch latch = new CountDownLatch(3);
long totalRecord[] = {0L};
long[] count = {0L};
//獲取瀏覽總?cè)藬?shù)
executor.execute(() -> {
count[0] = Optional.ofNullable(mongoTemplate.aggregate(Aggregation.newAggregation(operations), "visit_log", VisitRecordEntity.class))
.map(result -> result.getMappedResults()).map(mappedResult -> mappedResult.size()).orElse(0);
latch.countDown();
});
//獲取瀏覽記錄總數(shù)
executor.execute(() -> {
List<AggregationOperation> totalQueryOperations = new ArrayList<>();
totalQueryOperations.add(Aggregation.match(criteria));
totalQueryOperations.add(Aggregation.group().sum("viewCount").as("totalRecord"));
totalRecord[0] = (long) Optional.ofNullable(mongoTemplate.aggregate(Aggregation.newAggregation(totalQueryOperations), "visit_log", Map.class))
.map(result -> result.getMappedResults()).map(mappedResult -> mappedResult.get(0)).map(map -> map.get("totalRecord")).orElse(0);
latch.countDown();
});
//獲取瀏覽記錄詳情
List<VisitRecordEntity>[] recordEntities = new List[]{null};
executor.execute(() -> {
Aggregation agg =
Aggregation.newAggregation(
Aggregation.match(criteria),
Aggregation.group("viewer_name").sum("count").as("viewCount").first("viewer_name").as("viewerName")
.max("last_update_date").as("viewTime"),
Aggregation.sort(Sort.by(Sort.Direction.DESC, "viewTime")),
Aggregation.skip(offset),
Aggregation.limit(limit)
);
AggregationResults<VisitRecordEntity> aggregate = this.mongoTemplate.aggregate(agg, "visit_log", VisitRecordEntity.class);
recordEntities[0] = Optional.ofNullable(aggregate).map(AggregationResults::getMappedResults).orElse(new ArrayList<>(0));
latch.countDown();
});
try {
latch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
ResultEntity<VisitRecordEntity> resultEntity = new SearchResultEntity<>();
resultEntity.setItems(recordEntities[0]);
resultEntity.setTotalRecord(totalRecord[0]);
resultEntity.setTotalRow(count[0]);
return resultEntity;
}
總結(jié)
本文詳細(xì)介紹了SpringBoot如何整合MongoDB壶硅,并且結(jié)合博客系統(tǒng)的訪問(wèn)記錄展示了MongoTemplate的基本用法。