MongoTemplate配置
spring:
data:
mongodb:
uri: mongodb://root:password@ip1:27000,ip2:27000/test
一般情况下,按照如下配置,springboot会进行自动装配,但是如果需要实现一些自定义的功能,例如密码加解密,类型转换等功能需要手写配置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,即通过规范命名的接口来实现简单的DB操作,不需要自己写Query,可以通过该注解的value属性来指定注解的作用范围。
ReUtil是一个正则表达式的工具类,用于判断配置文件的格式是否正确,配置MongoDatabaseFactory过程中实现一个比较简单的配置文件解密的过程,解密方法用简单的字符串翻转来实现。
通过MappingMongoConverter来实现java中的对象与MongoDB中的Document进行一些复杂的映射,默认情况下一个java域对象存入MongoDB时会生成一个"_class"的key对应存储Java对象类型,通过
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
来取消每条记录生成一个"-class"的数据。
通过MappingMongoConverter实现一个简单的时间转化功能TimestampConverter,如下所示
@Component
public class TimestampConverter implements Converter {
@Override
public Timestamp convert(Date date) {
if (date != null) {
return new Timestamp(date.getTime());
}
return null;
}
}
还可以进行更加精细化的配置,例如
@WritingConverter
public class DateToString implements Converter {
@Override
public String convert(LocalDateTime source) {
return source.toString() + 'Z';
}
}
// Direction: MongoDB -> Java
@ReadingConverter
public class StringToDate implements Converter {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(source,DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
}
}
可以通过WritingConverter和ReadingConverter配置Document和Java对象相互转化。
MongoTemplate实战
例如一个博客系统,我们通过MongoDB存储用户的浏览记录,浏览记录的实体如下所示,
@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;
}
如上所示,每个人对应每篇文章有一条浏览记录,每次访问都会对访问次数viewCount进行+1操作.下文针对这个场景介绍MongoTemplate的基本操作。
- findOne,根据查询条件获取一个结果,返回第一个匹配的结果;
- exists,判断符合查询条件的记录是否存在;
- find,获取符合查询结果的记录;
- findAndRemove,查询符合条件的记录并删除。
这些操作用法基本一样,如下所示,传入一个封装查询条件的对象Query,Java中映射的对象entityClass和MongoDB中对应的Document的名称。
public T findOne(Query query, Class entityClass, String collectionName);
例如我们想要查询某个用户某篇博客的访问次数,我们只需要通过博客id和访问者构建查询条件进行查询即可。
public List 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 findAndModify(Query query, Update update, Class entityClass, String collectionName) {
return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName);
}
Query封装查询条件,Update封装的是更新内容。例如用户每次刷新页面浏览次数会+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;
}
}
如上所示,首先判断用户是否存在访问记录,如果存在则通过Update对访问次数viewCount进行+1操作,若不存在访问记录则新建访问记录。
保存操作包括主要包括insert和save方法,这两个方法都没有返回值,同时两个方法有一些区别,
- 单个记录插入时,如果新数据的主键已经存在,insert方法会报错DuplicateKeyException提示主键重复,不保存当前数据,而save方法会根据当前数据对存量数据进行更新;
- 进行批量保存时,insert方法可以一次性插入一个列表,不需要遍历,而save方法需要遍历列表进行一个一个的插入,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);
注释说明该方法的功能是,如果存在与查询条件匹配的文档,则根据Update中的内容进行更新,如果不存在符合查询条件的内容,则根据查询条件和Update插入新的文档。
聚合查询MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。本文侧重于Java实现。
结合上述中的访问记录的场景,如果我们需要统计某个博主某个专栏下面所有文章的访问记录,包括访问总人数,访问总次数,以及每个访客对应的访问次数详情,并且要满足分页需求,那么我们需要用到MongoDB的聚合操作,具体实现如下所示
public ResultEntity queryVisitRecord(List pageIds, long offset, int limit) {
Criteria criteria = Criteria.where("page_id").in(contentIds);
List operations = new ArrayList<>();
operations.add(Aggregation.match(criteria));
operations.add(Aggregation.group("viewer_name").sum("view_count").as("viewCount").first("viewer_name").as("viewerName"));
//多线程处理提高响应速度
CountDownLatch latch = new CountDownLatch(3);
long totalRecord[] = {0L};
long[] count = {0L};
//获取浏览总人数
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();
});
//获取浏览记录总数
executor.execute(() -> {
List 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[] 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 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 resultEntity = new SearchResultEntity<>();
resultEntity.setItems(recordEntities[0]);
resultEntity.setTotalRecord(totalRecord[0]);
resultEntity.setTotalRow(count[0]);
return resultEntity;
}
总结
本文详细介绍了SpringBoot如何整合MongoDB,并且结合博客系统的访问记录展示了MongoTemplate的基本用法。