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

這樣 一個統一的多數據源事務管理器就配置好了

檢驗

在這裏插入圖片描述
在這裏插入圖片描述
一旦出現異常 所有數據源插入數據都會進行回滾!!!因此 配置統一的事務管理器 完成!!!

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