美文网首页
SpringBoot整合MongoDB实战

SpringBoot整合MongoDB实战

作者: 花醉霜寒 | 来源:发表于2020-11-11 15:03 被阅读0次

    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<Date, Timestamp> {
        @Override
        public Timestamp convert(Date date) {
            if (date != null) {
                return new Timestamp(date.getTime());
            }
            return null;
        }
    }
    

    还可以进行更加精细化的配置,例如

    @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'"));
        }
    }
    

    可以通过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的基本操作。

    \color{green}{简单查询的相关操作}

    • findOne,根据查询条件获取一个结果,返回第一个匹配的结果;
    • exists,判断符合查询条件的记录是否存在;
    • find,获取符合查询结果的记录;
    • findAndRemove,查询符合条件的记录并删除。

    这些操作用法基本一样,如下所示,传入一个封装查询条件的对象Query,Java中映射的对象entityClass和MongoDB中对应的Document的名称。

    public <T> T findOne(Query query, Class<T> entityClass, String collectionName);
    
    

    例如我们想要查询某个用户某篇博客的访问次数,我们只需要通过博客id和访问者构建查询条件进行查询即可。

    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);
    }
    

    \color{green}{查找并更新}
    findAndModify表示更新符合查询条件的记录,其方法如下所示,

        public <T> T findAndModify(Query query, Update update, Class<T> 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操作,若不存在访问记录则新建访问记录。

    \color{green}{保存操作}
    保存操作包括主要包括insert和save方法,这两个方法都没有返回值,同时两个方法有一些区别,

    • 单个记录插入时,如果新数据的主键已经存在,insert方法会报错DuplicateKeyException提示主键重复,不保存当前数据,而save方法会根据当前数据对存量数据进行更新;
    • 进行批量保存时,insert方法可以一次性插入一个列表,不需要遍历,而save方法需要遍历列表进行一个一个的插入,insert方法的效率要高很多。

    \color{green}{upsert操作}
    该方法如下所示,

        /**
         * 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插入新的文档。

    \color{green}{聚合查询}
    聚合查询MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。本文侧重于Java实现。
    结合上述中的访问记录的场景,如果我们需要统计某个博主某个专栏下面所有文章的访问记录,包括访问总人数,访问总次数,以及每个访客对应的访问次数详情,并且要满足分页需求,那么我们需要用到MongoDB的聚合操作,具体实现如下所示

        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"));
            //多线程处理提高响应速度
            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<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;
        }
    

    总结
    本文详细介绍了SpringBoot如何整合MongoDB,并且结合博客系统的访问记录展示了MongoTemplate的基本用法。

    相关文章

      网友评论

          本文标题:SpringBoot整合MongoDB实战

          本文链接:https://www.haomeiwen.com/subject/oiiebktx.html