SpringBoot整合MongoDB(二)多数据源配置 分组 分页 统计 补 :事务使用

SpringBoot整合MongoDB(二)多数据源配置,Aggregation管道使用 事务使用

前言

若服务还未安装,请查看我的博客:Centos7 使用Yum源安装MongoDB4.2版本数据库(补:密码配置) 副本集搭建

若SpringBoot整合MongoDB基础还不会使用,请查看我的博客:SpringBoot整合MongoDB(一)

补:2020/05/12 本此在我一台服务器上 搭建了一个副本集 以支持事务 请查看上方mongo安装,副本集 搭建 blog

(一)多数据源配置

(1)所需依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
(2)yml配置
spring:
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  # 服务模块
  devtools:
    restart:
      # 热部署开关
      enabled: true
  data:
    mongodb:
      one:
        uri: mongodb://leilei:[email protected]:27017/dev1   #mongodb://账户:密码@ip:端口/数据库名
      two:
        uri: mongodb://leilei:[email protected]:27017/dev2
      three:
        uri: mongodb://leilei:[email protected]:27017/dev3
server:
  port: 8099
(3)Mongo初始化文件配置

1.mongo初始化加载配置类 因修改了yml中原本mongo配置写法,此类则告诉Spring从哪里找Mongo连接配置

package com.example.demo.config;

import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * @author : leilei
 * @date : 16:01 2020/2/16
 * @desc :  mongo连接配置类
 */
@Configuration
public class MongoInit {
    @Bean(name = "oneMongoProperties")
    @Primary  //必须设一个主库 不然会报错
    @ConfigurationProperties(prefix = "spring.data.mongodb.one")
    public MongoProperties statisMongoProperties() {
        System.out.println("-------------------- oneMongoProperties init ---------------------");
        return new MongoProperties();
    }

    @Bean(name = "twoMongoProperties")
    @ConfigurationProperties(prefix = "spring.data.mongodb.two")
    public MongoProperties twoMongoProperties() {
        System.out.println("-------------------- twoMongoProperties init ---------------------");
        return new MongoProperties();
    }

    @Bean(name = "threeMongoProperties")
    @ConfigurationProperties(prefix = "spring.data.mongodb.three")
    public MongoProperties threeMongoProperties() {
        System.out.println("-------------------- threeMongoProperties init ---------------------");
        return new MongoProperties();
    }
}

(4)多数据源连接配置
(1)第一个数据源
package com.example.demo.config;

import com.mongodb.MongoClientURI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

/**
 * @author : leilei
 * @date : 16:03 2020/2/16
 * @desc : monngo第一个数据源 basePackages 指向第二个数据源中所以有实体类对应的包路径,那么才操作该书体类时会直接影响Mongo对应库 例如 新增
 */
@Configuration
@EnableMongoRepositories(
        basePackages = "com.example.demo.entity.one",
        mongoTemplateRef = "oneMongo")
public class OneMongoMongoTemplate {

    @Autowired
    @Qualifier("oneMongoProperties")
    private MongoProperties mongoProperties;

    @Primary
    @Bean(name = "oneMongo")
    public MongoTemplate statisMongoTemplate() throws Exception {
        return new MongoTemplate(statisFactory(this.mongoProperties));
    }

    @Bean
    @Primary
    public MongoDbFactory statisFactory(MongoProperties mongoProperties) throws Exception {
        return new SimpleMongoDbFactory(new MongoClientURI(mongoProperties.getUri()));
    }
}

(2)第二个数据源
/**
 * @author : leilei
 * @date : 16:04 2020/2/16
 * @desc : mongo第二个数据源 basePackages 指向第二个数据源中所以有实体类对应的包路径,那么才操作该书体类时会直接影响Mongo对应库 例如 新增
 */
@Configuration
@EnableMongoRepositories(
        basePackages = "com.example.demo.entity.two",
        mongoTemplateRef = "twoMongo")
public class TwoMongoTemplate {

    @Autowired
    @Qualifier("twoMongoProperties")
    private MongoProperties mongoProperties;

    @Bean(name = "twoMongo")
    public MongoTemplate listTemplate() throws Exception {
        return new MongoTemplate(listFactory(this.mongoProperties));
    }

    @Bean
    public MongoDbFactory listFactory(MongoProperties mongoProperties) throws Exception {

        return new SimpleMongoDbFactory(new MongoClientURI(mongoProperties.getUri()));
    }
}

(3)第三个数据源
/**
 * @author : leilei
 * @date : 16:05 2020/2/16
 * @desc :mongo第三个数据源
 */
@Configuration
@EnableMongoRepositories(
        basePackages = "com.example.demo.entity.three",
        mongoTemplateRef = "threeMongo")
public class ThreeMongoTemplate {

    @Autowired
    @Qualifier("threeMongoProperties")
    private MongoProperties mongoProperties;

    @Bean(name = "threeMongo")
    public MongoTemplate listTemplate() throws Exception {
        return new MongoTemplate(ThreeFactory(this.mongoProperties));
    }

    @Bean
    public MongoDbFactory ThreeFactory(MongoProperties mongoProperties) throws Exception {

        return new SimpleMongoDbFactory(new MongoClientURI(mongoProperties.getUri()));
    }
}
(5)mongo监听 去除自带_class字段
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;

/**
 * @author : leilei
 * @date : 16:00 2020/2/16
 * @desc : mongo监听 新增时消除默认添加的 _class 字段保存实体类类型
 */
@Configuration
public class ApplicationReadyListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    @Qualifier("oneMongo")
    MongoTemplate oneMongoTemplate;

    @Autowired
    @Qualifier("twoMongo")
    MongoTemplate twoMongoTemplate;

    @Autowired
    @Qualifier("threeMongo")
    MongoTemplate threeMongoTemplate;

    private static final String TYPEKEY = "_class";

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        MongoConverter converter = oneMongoTemplate.getConverter();
        if (converter.getTypeMapper().isTypeKey(TYPEKEY)) {
            ((MappingMongoConverter) converter).setTypeMapper(new DefaultMongoTypeMapper(null));
        }
        MongoConverter converter2 = twoMongoTemplate.getConverter();
        if (converter2.getTypeMapper().isTypeKey(TYPEKEY)) {
            ((MappingMongoConverter) converter2).setTypeMapper(new DefaultMongoTypeMapper(null));
        }
        MongoConverter converter3 = threeMongoTemplate.getConverter();
        if (converter3.getTypeMapper().isTypeKey(TYPEKEY)) {
            ((MappingMongoConverter) converter3).setTypeMapper(new DefaultMongoTypeMapper(null));
        }
    }
}

到这里多数据源连接配置已经结束了

(6)多数据源下使用

在多数据源的情况下使用@Qualifier注解来具体选择使用哪一个数据源bean

在服务层注入

    @Autowired
    @Qualifier("oneMongo")
    private MongoTemplate oneMongoTemplate;

那么注入了第一个数据源,使用oneMongoTemplate 操作对应的实体类,编写服务代码即可

一个服务实现类使用多个数据源 需要多少数据源便注入其对应bean

@Service
public class StudentServiceImpl implements IStudentService {
    @Autowired
    @Qualifier("threeMongo")
    private MongoTemplate threeMongoTemplate;

    @Autowired
    @Qualifier("oneMongo")
    private MongoTemplate oneMongoTemplate;
    
    //示例
    @Override
    public int insertStudent(Student student) {
        LocalDateTime now = LocalDateTime.now();
        student.setCreatTime(now);
        try {
            threeMongoTemplate.insert(student);
            oneMongoTemplate.insert(
                    User.builder()
                            .id(student.getId())
                            .userName(student.getStudentName())
                            .age(student.getAge())
                            .creatTime(now)
                            .sex(new User().getSex())
                            .build());
            return 1;
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }
}

需要更多数据源则继续在yml文件中添加配置以及在MongoInit 类中加入其对应加载信息,并参照1 2 3 数据源添加更多

到此,多数据源配置以及使用就结束了

(二) Aggregation管道使用

(1)统计某一字段总和

此示例仅仅为了演示效果,无需关注逻辑问题(本文为所有玩家年龄之和)

接口

Integer countUserAge();

实现类

 /**
     * 统计所有用户年龄总和
     *
     * @return
     */
    @Override
    public Integer countUserAge() {
        Aggregation aggregation = newAggregation(group().sum("$age").as("ageSum"));
        AggregationResults<UserVo> user = oneMongoTemplate.aggregate(aggregation, "user", UserVo.class);
        if (user != null) {
            List<UserVo> mappedResults = user.getMappedResults();
            if (mappedResults != null) {
                UserVo userVo = mappedResults.get(0);
                return userVo.getAgeSum();
            }
            return null;
        }
        return null;
    }
(2)按照月份统计计数

接口

 MonthByUser countUserByMonth();

实现类

   /**
     * 根据月份统计玩家注册数
     *
     * @return
     */
    @Override
    public MonthByUser countUserByMonth() {
        List<AggregationOperation> operations = new ArrayList<AggregationOperation>();
        operations.add(Aggregation.project().andExpression("substr(creatTime,5,2)").as("creatTime"));
        operations.add(Aggregation.group("creatTime").count().as("total"));
        Aggregation aggregation = Aggregation.newAggregation(operations);
        AggregationResults<UserVo> aggregate =
                oneMongoTemplate.aggregate(aggregation, "user", UserVo.class);
        if (aggregate != null) {
            MonthByUser monthByUser = new MonthByUser();
            //判断月份 存值
            aggregate.getMappedResults().forEach(e -> {
                if (e.getId() == 1) {
                    monthByUser.setJanuary(e.getTotal());
                }
                if (e.getId() == 2) {
                    monthByUser.setFebruary(e.getTotal());
                }
                if (e.getId() == 3) {
                    monthByUser.setMarch(e.getTotal());
                }
                if (e.getId() == 4) {
                    monthByUser.setApril(e.getTotal());
                }
                if (e.getId() == 5) {
                    monthByUser.setMay(e.getTotal());
                }
                if (e.getId() == 6) {
                    monthByUser.setJune(e.getTotal());
                }
                if (e.getId() == 7) {
                    monthByUser.setJuly(e.getTotal());
                }
                if (e.getId() == 8) {
                    monthByUser.setAugust(e.getTotal());
                }
                if (e.getId() == 9) {
                    monthByUser.setSeptember(e.getTotal());
                }
                if (e.getId() == 10) {
                    monthByUser.setOctober(e.getTotal());
                }
                if (e.getId() == 11) {
                    monthByUser.setNovember(e.getTotal());
                }
                if (e.getId() == 12) {
                    monthByUser.setDecember(e.getTotal());
                }
            });
            return monthByUser;
        }
        return MonthByUser.builder()
                .February(0).January(0).March(0)
                .April(0).May(0).June(0).July(0)
                .August(0).September(0).October(0)
                .November(0).December(0).build();
    }

代码含义以及注意事项

substr(creatTime,5,2)代码含义:

​ 截取creatTime 字段的值 从第五位开始截取 一共取两位 并将字段设置别名 别名也为creatTime

注意1: substr是我根据某一字段的值进行切割 ,从第五开始算 共截取两个 并取别名

注意2:下列代码应该看到,e.getId()与 1-12做比较 是为判别对应月份

通过我Substr后 使用Aggregation管道查询出的每个UserVo对象的id 变为了对应的月份

UserVo(id=2, userName=null, sex=null, age=null, creatTime=null, ageSum=null, total=6)
UserVo(id=3, userName=null, sex=null, age=null, creatTime=null, ageSum=null, total=3)

上方代码则为 2月有6人注册 3月有3人注册

数据库数据图如下:
在这里插入图片描述

(3)统计报表 日 周 月 季 年 总 某一字段数据之和

请注意代码 不要纠结统计 今日 或者本年 玩家年龄和的逻辑性

接口

CountUser countUser();

实现类

  /***
     * 今日 本周 本月 本季 本年 总  (玩家年龄之和)
     * @return
     */
    @Override
    public CountUser countUser() {
        /** 条件 */
        Criteria criDay =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfDay(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfDay(DateUtil.date())));
        Criteria criWeek =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfWeek(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfWeek(DateUtil.date())));

        Criteria criMonth =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfMonth(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfMonth(DateUtil.date())));
        Criteria criQuarter =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfQuarter(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfQuarter(DateUtil.date())));
        Criteria criYear =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfYear(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfYear(DateUtil.date())));


        /** 逻辑判断 */
        Cond condDay = ConditionalOperators.when(criDay).thenValueOf("$age").otherwise(0);
        ConditionalOperators.Cond condWeek = ConditionalOperators.when(criWeek).thenValueOf("$age").otherwise(0);
        Cond condMonth = ConditionalOperators.when(criMonth).thenValueOf("$age").otherwise(0);
        Cond condQuarter = ConditionalOperators.when(criQuarter).thenValueOf("$age").otherwise(0);
        Cond condyear = ConditionalOperators.when(criYear).thenValueOf("$age").otherwise(0);

        /** 分组 查询*/
        Aggregation agg =
                Aggregation.newAggregation(
                        Aggregation.match(Criteria.where("sex").is("男")),
                        Aggregation.group()
                                .sum(condDay)
                                .as("dayCount")
                                .sum(condWeek)
                                .as("weekCount")
                                .sum(condMonth)
                                .as("monthCount")
                                .sum(condQuarter)
                                .as("quarterCount")
                                .sum(condyear)
                                .as("yearCount")
                                .sum("$age")
                                .as("totalCount"),
                        Aggregation.skip(0),
                        Aggregation.limit(1));
        AggregationResults<CountUser> aggregate =
                oneMongoTemplate.aggregate(agg, "user", CountUser.class);
        if (aggregate != null) {
            if (aggregate.getMappedResults().size() > 0) {
                return aggregate.getMappedResults().get(0);
            }
            return CountUser.builder()
                    .dayCount(0)
                    .weekCount(0)
                    .monthCount(0)
                    .quarterCount(0)
                    .yearCount(0)
                    .totalCount(0)
                    .build();
        }
        return CountUser.builder()
                .dayCount(0)
                .weekCount(0)
                .monthCount(0)
                .quarterCount(0)
                .yearCount(0)
                .totalCount(0)
                .build();
    }

惊叹一下

Hutool工具包是真的超级无敌强大,里边的工具类实在是用着太爽了,减少了好多开发时间,又可以偷闲了。。。哈哈哈哈

代码含义讲解以及注意事项

        Criteria criDay =
                Criteria.where("creatTime")
                        .andOperator(
                                Criteria.where("creatTime").gte(DateUtil.beginOfDay(DateUtil.date())),
                                Criteria.where("creatTime").lte(DateUtil.endOfDay(DateUtil.date())));

意义为查询符合 creatTime 字段值在 今日开始以及今日结束的数据

DateUtil是我引用的hutool工具包

Aggregation.match 则为查询条件等效于query中的Criteria 例如 本示例的Aggregation.match(Criteria.where(“sex”).is(“男”)) 则查询性别为男的 今日 月 年 总,,,年龄总和

如果想要根据某一字段进行分组统计总和 则依葫芦画瓢添加使用
Aggregation.group(“分组字段”) 即可

Aggregation.skip(0) 与之前query 含义一致 跳过多少个数据

Aggregation.limit(1); 与之前query 含义一致 展示多少个数据

上述两个在我目前代码并无意义,因为我这种未分组统计只会有一个数据,我仅仅是为了演示其分页如何操作

比如每页十条数据那么 展示第二页 那么写法则为 Aggregation.skip(10) Aggregation.limit(10)

分页 skip公式: (当前页-1)* 每页展示数据长度

Aggregation.sort 排序 用法与query中一致

CountUser为我自定义的分页统计响应对象

逻辑判断Cond

Cond condDay = ConditionalOperators.when(criDay).thenValueOf("$age").otherwise(0);

当前代码为通过条件对象criDay 去统计age 这一组 如果criDay 条件满足则使用thenValueOf 对某一字段的值进行统计 条件不满足则使用otherwise中的数据0进行显示填充

那么本文的Aggregation一些基本用法和SpringBoot整合MongoDB就先展示到这里了

附上我的源码:springboot-mongo-moredatasource

补副本集下使用 (单服务器单副本集 事务测试)

副本集搭建 请看我上方的 mongodb4.2 安装了 ,我在里边有补充

(1)修改咱们yml 配置文件中 mongodb 的连接方式

mongodb://用户:密码@Ip:27017/dev1?replicaSet=副本集名&authSource=admin&authMechanism=SCRAM-SHA-1

实际就是在原来基础上 添加了 副本集名

(2)配置事务管理器

在我之前的Mongotemplate配置文件中 添加各自的事务管理器
这里贴出第一个数据源的事务管理器配置 其他源按照这个配
在这里插入图片描述

    @Bean(name = "statisTransactionManager")
    MongoTransactionManager statisTransactionManager() throws Exception {
        MongoDbFactory mongoDbFactory = statisFactory(this.mongoProperties);
        return new MongoTransactionManager(mongoDbFactory);
    }
    

接下来 开始事务测试

(3)事务测试

在这里插入图片描述
先清空数据库 然后使用postman 进行测试
在这里插入图片描述
使用postman 进行测试后 ,控制台已经是打印了错误信息 ,接下来 我们查看数据库
在这里插入图片描述
在这里插入图片描述
额… 数据居然插入进去了???!!!,我第一反应 这副本集 和mongo事务管理 管理器假的吧
找了很久文档 居然数据未回滚的原因是因为我使用了try catch 原来 在捕获异常情况下 ,为了让Mongo 数据回滚 还需要自己catch的地方手动添加一行代码

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

在这里插入图片描述
我们在测一测
在这里插入图片描述
控制台再次打印了控制信息
在这里插入图片描述
我们再看数据库 看是否有插入的事务二号信息 如果没有 那就说明生效了
在这里插入图片描述
只有事务一号 文档 说明二号已经回滚了 那么这个数据源的事务就算完成了!

个人遇到的问题

多个数据源 配置 每个方法中 只向一个数据源写入数据 均会回滚 但是 ,一个方法中 向多个数据源写入 也只会回滚 指定事务管理器 下数据源

(1) 不想每次写 catch 中回滚的代码怎么办? 已解决

在不使用捕获 或者try catch 时候 可以不用写 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

(2) 在同一个方法中 同时向多个数据源中写入,只会回滚当前方法指定的一个事务管理器 下的数据源数据 ? 未解决 请大哥们帮助啊!!

在这里插入图片描述
待续: 多机器下的副本集搭建 以及 多数据源配置统一事务管理器,达到多数据源 同时回滚。。。。。

2020/05/18补: 统一事务管理器

上文中讲到了 多数据源项目中 在每个方法内 操作一个数据源 是可以进行回滚的,但是 如果一个方法中 操作多个数据源 就不会回滚了。。我的补充主要解决 同一方法多数据源事务不生效问题

由于我们每个数据源都配了对应的事务管理器 所以单个方法开启事务只需要指定其对应事务管理器即可
那么多数据源事务呢 ,其实也是可以配置一个统一的事务管理器的
那就是使用 ChainedTransactionManager 此构造方法为 事务管理器 可变参数

那么我们就来实践一波 将我们上边配置好的各个参数管理器 作为参数 传到 ChainedTransactionManager 中

  @Bean(name = "chainedTransactionManager")
  public ChainedTransactionManager transactionManager(
      @Qualifier("oneTransactionManager") PlatformTransactionManager ds1,
      @Qualifier("twoTransactionManager") PlatformTransactionManager ds2,
      @Qualifier("threeTransactionManager") PlatformTransactionManager ds3) {
    return new ChainedTransactionManager(ds1, ds2, ds3);
  }

这样 一个统一的多数据源事务管理器就配置好了

检验

在这里插入图片描述
在这里插入图片描述
一旦出现异常 所有数据源插入数据都会进行回滚!!!因此 配置统一的事务管理器 完成!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章