SpringBoot整合MongoDB实战

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的基本用法。

你可能感兴趣的:(SpringBoot整合MongoDB实战)