文章目录

  • SpringBoot整合MongoDB(二)多数据源配置,Aggregation管道使用 事务使用
  • (一)多数据源配置
  • (1)所需依赖
  • (2)yml配置
  • (3)Mongo初始化文件配置
  • (4)多数据源连接配置
  • (1)第一个数据源
  • (2)第二个数据源
  • (3)第三个数据源
  • (5)mongo监听 去除自带_class字段
  • (二) Aggregation管道使用
  • (1)统计某一字段总和
  • (2)按照月份统计计数
  • (3)统计报表 日 周 月 季 年 总 某一字段数据之和
  • 补副本集下使用 (单服务器单副本集 事务测试)
  • (1)修改咱们yml 配置文件中 mongodb 的连接方式
  • (2)配置事务管理器
  • (3)事务测试
  • 个人遇到的问题
  • (1) 不想每次写 catch 中回滚的代码怎么办? 已解决
  • (2) 同方法中多数据源事务失效
  • 2020/05/18补: 统一事务管理器


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:xxxx@47.97.118.22:27017/dev1   #mongodb://账户:密码@ip:端口/数据库名
      two:
        uri: mongodb://leilei:xxxx@47.97.118.22:27017/dev2
      three:
        uri: mongodb://leilei:xxxx@47.97.118.22: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人注册

数据库数据图如下:

springboot使用MongoDB分页 springboot mongodb分片_数据库

(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配置文件中 添加各自的事务管理器

这里贴出第一个数据源的事务管理器配置 其他源按照这个配

springboot使用MongoDB分页 springboot mongodb分片_spring_02

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

接下来 开始事务测试

(3)事务测试

springboot使用MongoDB分页 springboot mongodb分片_mongodb_03


先清空数据库 然后使用postman 进行测试

springboot使用MongoDB分页 springboot mongodb分片_mongodb_04


使用postman 进行测试后 ,控制台已经是打印了错误信息 ,接下来 我们查看数据库

springboot使用MongoDB分页 springboot mongodb分片_数据库_05


springboot使用MongoDB分页 springboot mongodb分片_数据库_06


额… 数据居然插入进去了???!!!,这是为什么呢?

因为我使用了try catch,捕获后没有做任何处理,导致异常被吞掉了;Spring申明式事务要求我们不进行异常捕获

如果非要在捕获异常情况下进行事务回滚 ,必须在catch的地方手动添加一行代码

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

springboot使用MongoDB分页 springboot mongodb分片_数据源_07


我们在测一测

springboot使用MongoDB分页 springboot mongodb分片_数据库_08


控制台再次打印了控制信息

springboot使用MongoDB分页 springboot mongodb分片_spring_09


我们再看数据库 看是否有插入的事务二号信息 如果没有 那就说明生效了

springboot使用MongoDB分页 springboot mongodb分片_spring boot_10


只有事务一号 文档 说明二号已经回滚了 那么这个数据源的事务就算完成了!

个人遇到的问题

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

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

不使用手动捕获异常try catch

直接使用 throws 抛出异常,借助全局异常拦截,交由spring申明式事物帮我们处理

springboot使用MongoDB分页 springboot mongodb分片_mongodb_11

(2) 同方法中多数据源事务失效

springboot使用MongoDB分页 springboot mongodb分片_数据库_12


待续: 多机器下的副本集搭建 以及 多数据源配置统一事务管理器,达到多数据源 同时回滚。。。。。

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

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

springboot使用MongoDB分页 springboot mongodb分片_数据源_13

springboot使用MongoDB分页 springboot mongodb分片_数据源_14


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