SpringBoot整合MongoDB實(shí)戰(zhàn)

MongoTemplate配置

spring:
  data:
    mongodb:
      uri: mongodb://root:password@ip1:27000,ip2:27000/test

一般情況下柑司,按照如下配置糟袁,springboot會(huì)進(jìn)行自動(dòng)裝配乱豆,但是如果需要實(shí)現(xiàn)一些自定義的功能辜窑,例如密碼加解密钩述,類型轉(zhuǎn)換等功能需要手寫(xiě)配置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穆碎,即通過(guò)規(guī)范命名的接口來(lái)實(shí)現(xiàn)簡(jiǎn)單的DB操作牙勘,不需要自己寫(xiě)Query,可以通過(guò)該注解的value屬性來(lái)指定注解的作用范圍所禀。
ReUtil是一個(gè)正則表達(dá)式的工具類方面,用于判斷配置文件的格式是否正確,配置MongoDatabaseFactory過(guò)程中實(shí)現(xiàn)一個(gè)比較簡(jiǎn)單的配置文件解密的過(guò)程色徘,解密方法用簡(jiǎn)單的字符串翻轉(zhuǎn)來(lái)實(shí)現(xiàn)恭金。
通過(guò)MappingMongoConverter來(lái)實(shí)現(xiàn)java中的對(duì)象與MongoDB中的Document進(jìn)行一些復(fù)雜的映射,默認(rèn)情況下一個(gè)java域?qū)ο蟠嫒隡ongoDB時(shí)會(huì)生成一個(gè)"_class"的key對(duì)應(yīng)存儲(chǔ)Java對(duì)象類型褂策,通過(guò)

 mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));

來(lái)取消每條記錄生成一個(gè)"-class"的數(shù)據(jù)横腿。
通過(guò)MappingMongoConverter實(shí)現(xiàn)一個(gè)簡(jiǎn)單的時(shí)間轉(zhuǎn)化功能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;
    }
}

還可以進(jìn)行更加精細(xì)化的配置辙培,例如

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

可以通過(guò)WritingConverter和ReadingConverter配置Document和Java對(duì)象相互轉(zhuǎn)化蔑水。

MongoTemplate實(shí)戰(zhàn)

例如一個(gè)博客系統(tǒng),我們通過(guò)MongoDB存儲(chǔ)用戶的瀏覽記錄扬蕊,瀏覽記錄的實(shí)體如下所示搀别,

@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;

}

如上所示,每個(gè)人對(duì)應(yīng)每篇文章有一條瀏覽記錄尾抑,每次訪問(wèn)都會(huì)對(duì)訪問(wèn)次數(shù)viewCount進(jìn)行+1操作.下文針對(duì)這個(gè)場(chǎng)景介紹MongoTemplate的基本操作歇父。

\color{green}{簡(jiǎn)單查詢的相關(guān)操作}

  • findOne,根據(jù)查詢條件獲取一個(gè)結(jié)果再愈,返回第一個(gè)匹配的結(jié)果膳凝;
  • exists刺洒,判斷符合查詢條件的記錄是否存在;
  • find,獲取符合查詢結(jié)果的記錄独泞;
  • findAndRemove,查詢符合條件的記錄并刪除亡笑。

這些操作用法基本一樣脸狸,如下所示,傳入一個(gè)封裝查詢條件的對(duì)象Query缴渊,Java中映射的對(duì)象entityClass和MongoDB中對(duì)應(yīng)的Document的名稱赏壹。

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

例如我們想要查詢某個(gè)用戶某篇博客的訪問(wèn)次數(shù),我們只需要通過(guò)博客id和訪問(wèn)者構(gòu)建查詢條件進(jìn)行查詢即可衔沼。

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封裝的是更新內(nèi)容菩佑。例如用戶每次刷新頁(yè)面瀏覽次數(shù)會(huì)+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;
    }
}

如上所示擎鸠,首先判斷用戶是否存在訪問(wèn)記錄缀磕,如果存在則通過(guò)Update對(duì)訪問(wèn)次數(shù)viewCount進(jìn)行+1操作,若不存在訪問(wèn)記錄則新建訪問(wèn)記錄劣光。

\color{green}{保存操作}
保存操作包括主要包括insert和save方法袜蚕,這兩個(gè)方法都沒(méi)有返回值,同時(shí)兩個(gè)方法有一些區(qū)別绢涡,

  • 單個(gè)記錄插入時(shí)牲剃,如果新數(shù)據(jù)的主鍵已經(jīng)存在,insert方法會(huì)報(bào)錯(cuò)DuplicateKeyException提示主鍵重復(fù)雄可,不保存當(dāng)前數(shù)據(jù)凿傅,而save方法會(huì)根據(jù)當(dāng)前數(shù)據(jù)對(duì)存量數(shù)據(jù)進(jìn)行更新;
  • 進(jìn)行批量保存時(shí)数苫,insert方法可以一次性插入一個(gè)列表聪舒,不需要遍歷,而save方法需要遍歷列表進(jìn)行一個(gè)一個(gè)的插入虐急,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);

注釋說(shuō)明該方法的功能是止吁,如果存在與查詢條件匹配的文檔被辑,則根據(jù)Update中的內(nèi)容進(jìn)行更新,如果不存在符合查詢條件的內(nèi)容敬惦,則根據(jù)查詢條件和Update插入新的文檔盼理。

\color{green}{聚合查詢}
聚合查詢MongoDB 中聚合(aggregate)主要用于處理數(shù)據(jù)(諸如統(tǒng)計(jì)平均值,求和等)俄删,并返回計(jì)算后的數(shù)據(jù)結(jié)果宏怔。本文側(cè)重于Java實(shí)現(xiàn)。
結(jié)合上述中的訪問(wèn)記錄的場(chǎng)景畴椰,如果我們需要統(tǒng)計(jì)某個(gè)博主某個(gè)專欄下面所有文章的訪問(wèn)記錄臊诊,包括訪問(wèn)總?cè)藬?shù),訪問(wèn)總次數(shù)迅矛,以及每個(gè)訪客對(duì)應(yīng)的訪問(wèn)次數(shù)詳情,并且要滿足分頁(yè)需求潜叛,那么我們需要用到MongoDB的聚合操作秽褒,具體實(shí)現(xiàn)如下所示

    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"));
        //多線程處理提高響應(yīng)速度
        CountDownLatch latch = new CountDownLatch(3);
        long totalRecord[] = {0L};
        long[] count = {0L};
        //獲取瀏覽總?cè)藬?shù)
        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();
        });

        //獲取瀏覽記錄總數(shù)
        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;
    }

總結(jié)
本文詳細(xì)介紹了SpringBoot如何整合MongoDB壶硅,并且結(jié)合博客系統(tǒng)的訪問(wèn)記錄展示了MongoTemplate的基本用法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末销斟,一起剝皮案震驚了整個(gè)濱河市庐椒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚂踊,老刑警劉巖约谈,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異犁钟,居然都是意外死亡棱诱,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)涝动,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)迈勋,“玉大人,你說(shuō)我怎么就攤上這事醋粟∶夜剑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵米愿,是天一觀的道長(zhǎng)厦凤。 經(jīng)常有香客問(wèn)我,道長(zhǎng)育苟,這世上最難降的妖魔是什么较鼓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮宙搬,結(jié)果婚禮上笨腥,老公的妹妹穿的比我還像新娘。我一直安慰自己勇垛,他們只是感情好脖母,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著闲孤,像睡著了一般谆级。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讼积,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天肥照,我揣著相機(jī)與錄音,去河邊找鬼勤众。 笑死舆绎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的们颜。 我是一名探鬼主播吕朵,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼猎醇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了努溃?” 一聲冷哼從身側(cè)響起硫嘶,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梧税,沒(méi)想到半個(gè)月后沦疾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡第队,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年哮塞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斥铺。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡彻桃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晾蜘,到底是詐尸還是另有隱情邻眷,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布剔交,位于F島的核電站肆饶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏岖常。R本人自食惡果不足惜驯镊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望竭鞍。 院中可真熱鬧板惑,春花似錦、人聲如沸偎快。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)晒夹。三九已至裆馒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丐怯,已是汗流浹背喷好。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留读跷,地道東北人梗搅。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親无切。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蟀俊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 摘要:開(kāi)始前,建議大家去了解以下文章订雾,當(dāng)然不看也沒(méi)問(wèn)題: MongoDB從入門(mén)到“精通”之簡(jiǎn)介和如何安裝 Mong...
    暖夏未眠丶閱讀 737評(píng)論 0 2
  • 基本介紹 什么是NoSQL數(shù)據(jù)庫(kù) NoSQL,指的是非關(guān)系型的數(shù)據(jù)庫(kù)矛洞。NoSQL有時(shí)也稱作Not Only SQL...
    我就是小政政閱讀 2,128評(píng)論 0 11
  • 隨著數(shù)據(jù)量的不斷上漲洼哎,項(xiàng)目需要快速處理數(shù)據(jù)成為了第一要?jiǎng)?wù),對(duì)于數(shù)據(jù)統(tǒng)計(jì)的嚴(yán)格性要求不高沼本。 MongoDb應(yīng)運(yùn)而生噩峦,...
    Tim在路上閱讀 3,397評(píng)論 2 15
  • 最近在準(zhǔn)備畢業(yè)論文,論文中使用到了Spring框架抽兆,考慮到數(shù)據(jù)的格式并非有固定的字段格式识补,因此考慮到使用Mongo...
    _挑燈看劍_閱讀 16,003評(píng)論 2 44
  • 久違的晴天,家長(zhǎng)會(huì)辫红。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí)凭涂,離放學(xué)已經(jīng)沒(méi)多少時(shí)間了。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)贴妻。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,523評(píng)論 16 22