SpringBoot 2.X MongoTemplate group aggregation query

Keywords: Java MongoDB Spring Boot Back-end nosql


1 Summary

How does MongoDB implement aggregate queries? Group query? Group paging query? User defined time interval query? Time format conversion query? Don't panic. This article will introduce the implementation of grouping and aggregation query of various dimensions based on SpringBoot 2.X MongoTemplate, and catch all the complex query difficulties of MongoDB.

2. Group query by fixed field

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query blog data by author group
     *
     * @param queryGroupByAuthorParam
     * @return
     */
    @Override
    public ApiResult queryGroupByAuthor(BlogQueryGroupByAuthorParam queryGroupByAuthorParam) {
        // query criteria
        Criteria criteria = new Criteria();
        if (StrUtil.isNotBlank(queryGroupByAuthorParam.getAuthor())) {
            criteria.and("author").is(queryGroupByAuthorParam.getAuthor());
        }
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("id", "author", "countRead", "countLike");
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group("author")
                .first("author").as("author")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // Query results
        AggregationResults<BlogSummaryVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                BlogEntity.class,matchOperation, projectionOperation, groupOperation), BlogSummaryVo.class);
        log.info("Query results: {}", results.getMappedResults());
        return ApiResult.success(results.getMappedResults());
    }

matters needing attention:

Grouped fields must be in ProjectionOperation

3 grouping and paging query

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query your blog data in groups and pages according to authors
     *
     * @param queryGroupByAuthorPageParam
     * @return
     */
    @Override
    public ApiResult queryGroupByAuthorPage(BlogQueryGroupByAuthorPageParam queryGroupByAuthorPageParam) {
        // query criteria
        Criteria criteria = new Criteria();
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("id", "author","countRead", "countLike");
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group("author")
                .first("author").as("author")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // Total number of queries
        GroupOperation groupOperation2 = Aggregation.group().count().as("totalAuthor");
        AggregationResults<Map> totalAuthorResult = mongoTemplate.aggregate(Aggregation.newAggregation(BlogEntity.class,
                matchOperation, projectionOperation, groupOperation, groupOperation2), Map.class);
        int totalAuthor = (int) totalAuthorResult.getUniqueMappedResult().getOrDefault("totalAuthor", 0);
        // Set paging information
        SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.DESC, "author"));
        SkipOperation skipOperation = Aggregation.skip((queryGroupByAuthorPageParam.getCurrentPage() - 1)
                * (long) queryGroupByAuthorPageParam.getPageSize());
        LimitOperation limitOperation = Aggregation.limit(queryGroupByAuthorPageParam.getPageSize());
        // Query results
        AggregationResults<BlogSummaryVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                BlogEntity.class, matchOperation, projectionOperation, groupOperation, sortOperation,
                skipOperation, limitOperation), BlogSummaryVo.class);
        // Assembly paging results
        Page<BlogSummaryVo> page = PageableExecutionUtils.getPage(results.getMappedResults(),
                PageRequest.of(queryGroupByAuthorPageParam.getCurrentPage() -1,
                        queryGroupByAuthorPageParam.getPageSize()), () -> totalAuthor);
        return ApiResult.success(page);
    }

matters needing attention:

Spring's paging class org.springframework.data.domain.Pageable calculates the first page from 0

4. Query by date group (the field is the timestamp accurate to seconds)

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query blog data by client time group
     *
     * @param queryGroupByClientTimestampParam
     * @return
     */
    @Override
    public ApiResult queryGroupByClientTimestamp(BlogQueryGroupByClientTimestampParam
                                                             queryGroupByClientTimestampParam) {
        // Get date grouping information
        BlogGroupDateVo groupDateVo = getGroupDate(queryGroupByClientTimestampParam.getDateType());
        // query criteria
        Criteria criteria = Criteria.where("clientTimestamp").gte(queryGroupByClientTimestampParam
                        .getMinClientTimestamp()).lte(queryGroupByClientTimestampParam.getMaxClientTimestamp());
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("countRead","countLike","clientTimestamp")
                .and(DateOperators.DateToString.dateOf(aggregationOperationContext ->
                                new Document("$add", Arrays.asList(new Date(28800000),
                                        new Document("$multiply", Arrays.asList("$clientTimestamp", 1000)))))
                        .toString(groupDateVo.getDateFormat())).as(groupDateVo.getDateGroupField());
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group(groupDateVo.getDateGroupField())
                .first(groupDateVo.getDateGroupField()).as("date")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // sort
        SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "date"));
        // Query results
        AggregationResults<BlogSummaryVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                BlogEntity.class, matchOperation, projectionOperation, groupOperation, sortOperation),
                BlogSummaryVo.class);
        return ApiResult.success(results.getMappedResults());
    }

matters needing attention:

MongoDB time is calculated according to UTC time zone. The basic unit is milliseconds. The domestic time zone is 8 hours earlier than UTC time, so it needs to be multiplied by 1000 and then 8 hours. 28800000 = 8 * 60 * 60 * 1000 (MS)

5 query by date group (the field is the timestamp accurate to milliseconds)

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query blog data by creation time
     *
     * @param queryGroupByCreateTimeParam
     * @return
     */
    @Override
    public ApiResult queryGroupByCreateTime(BlogQueryGroupByCreateTimeParam queryGroupByCreateTimeParam) {
        // Get date grouping information
        BlogGroupDateVo groupDateVo = getGroupDate(queryGroupByCreateTimeParam.getDateType());
        // query criteria
        Criteria criteria = Criteria.where("createTime").gte(queryGroupByCreateTimeParam.getMinCreateTime())
                .lte(queryGroupByCreateTimeParam.getMaxCreateTime());
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("countRead", "countLike","createTime")
                .and(DateOperators.DateToString.dateOf(aggregationOperationContext ->
                                new Document("$add", Arrays.asList(new Date(28800000),"$createTime")))
                        .toString(groupDateVo.getDateFormat())).as(groupDateVo.getDateGroupField());
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group(groupDateVo.getDateGroupField())
                .first(groupDateVo.getDateGroupField()).as("date")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // sort
        SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "date"));
        // Query results
        AggregationResults<BlogSummaryVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                BlogEntity.class, matchOperation, projectionOperation, groupOperation, sortOperation),
                BlogSummaryVo.class);
        return ApiResult.success(results.getMappedResults());
    }

matters needing attention:

The operation tool class of date to string provided by Spring for MongoDB is org.springframework.data.mongodb.core.aggregation.DateOperators. This class provides time intervals of multiple dimensions, including minutes, hours, days, weeks, months, custom date formats, etc

6 group query by date (field is Date)

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query blog data in groups according to update time
     *
     * @param queryGroupByUpdateTimeParam
     * @return
     */
    @Override
    public ApiResult queryGroupByUpdateTime(BlogQueryGroupByUpdateTimeParam queryGroupByUpdateTimeParam) {
        // query criteria
        Criteria criteria = Criteria.where("updateTime").gte(new Date(queryGroupByUpdateTimeParam.getMinUpdateTime()))
                .lte(new Date(queryGroupByUpdateTimeParam.getMaxUpdateTime()));
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("countRead", "countLike", "updateTime")
                .and(DateOperators.DayOfMonth.dayOfMonth("updateTime")).as("day")
                .and(DateOperators.Month.monthOf("updateTime")).as("month")
                .and(DateOperators.Year.yearOf("updateTime")).as("year");
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group("year","month","day")
                .addToSet("year").as("year")
                .addToSet("month").as("month")
                .addToSet("day").as("date")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // sort
        SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "_id.year",
                        "_id.month", "_id.day"));
        // Query results
        AggregationResults<BlogSummaryDiyVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                        BlogEntity.class, matchOperation, projectionOperation, groupOperation, sortOperation),
                BlogSummaryDiyVo.class);
        return ApiResult.success(results.getMappedResults());
    }

matters needing attention:

  • If the database field is java.util.Date, the date information can be obtained directly from the field without conversion

  • To sort multiple fields, you need to sort by_ Write in id.field format, otherwise an error may be reported; Sorting priority is to sort the fields from left to right

7 group query by user-defined time interval

The commonly used time grouping intervals are hour, day and month. The time interval supported by Spring is enough. However, when the time interval needs to be customized for specific scenarios, the time interval provided by Spring is not enough. The author also checked the whole network to find a solution. Write it down quickly so as not to forget it next time

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Query blog data by user-defined interval based on creation time
     *
     * @param queryGroupByCreateTimeDiyParam
     * @return
     */
    @Override
    public ApiResult queryGroupByCreateTimeDiy(BlogQueryGroupByCreateTimeDiyParam queryGroupByCreateTimeDiyParam) {
        // query criteria
        Criteria criteria = Criteria.where("createTime").gte(queryGroupByCreateTimeDiyParam.getMinCreateTime())
                .lte(queryGroupByCreateTimeDiyParam.getMaxCreateTime());
        MatchOperation matchOperation = Aggregation.match(criteria);
        // Query field
        ProjectionOperation projectionOperation = Aggregation.project("countRead", "countLike","createTime")
                .and(DateOperators.Minute.minuteOf(aggregationOperationContext ->
                                new Document("$add", Arrays.asList(new Date(28800000),"$createTime"))))
                        .as("minute")
                .and(DateOperators.Hour.hourOf(aggregationOperationContext ->
                        new Document("$add", Arrays.asList(new Date(28800000),"$createTime"))))
                .as("hour")
                .and(DateOperators.DateToString.dateOf(aggregationOperationContext ->
                                new Document("$add", Arrays.asList(new Date(28800000),"$createTime")))
                        .toString("%Y-%m-%d")).as("day");
        ProjectionOperation projectionOperation2 = Aggregation.project("countRead", "countLike",
                "createTime", "minute", "hour","day")
                .andExpression("minute - minute % 30").as("halfHour");
        // Grouping statistics
        GroupOperation groupOperation = Aggregation.group("day","hour","halfHour")
                .addToSet("hour").as("hour")
                .addToSet("day").as("date")
                .addToSet("halfHour").as("minute")
                .count().as("totalBlog")
                .sum("countRead").as("totalRead")
                .avg("countRead").as("aveRead")
                .sum("countLike").as("totalLike")
                .avg("countLike").as("aveLike");
        // sort
        SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "day", "hour", "halfHour"));
        // Query results
        AggregationResults<BlogSummaryDiyVo> results = mongoTemplate.aggregate(Aggregation.newAggregation(
                        BlogEntity.class, matchOperation, projectionOperation, projectionOperation2,
                        groupOperation, sortOperation), BlogSummaryDiyVo.class);
        return ApiResult.success(results.getMappedResults());
    }

matters needing attention:

  • All fields of the previous must be included in the second ProjectionOperation

  • The time interval in this example is 30 minutes

8 database entity class and other related classes

8.1 database entity class

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/model/entity/BaseEntity.java
package com.ljq.demo.springboot.mongodb.model.entity;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.Field;

import java.io.Serializable;
import java.util.Date;

/**
 * @Description: Base entity class
 * @Author: junqiang.lu
 * @Date: 2021/9/24
 */
@Data
public class BaseEntity implements Serializable {

    private static final long serialVersionUID = -3003658740476069858L;

    /**
     * id,Primary key
     */
    @Id
    @ApiModelProperty(value = "id,Primary key", name = "id")
    private String id;
    /**
     * Creation time
     */
    @Field
    @CreatedDate
    @ApiModelProperty(value = "Creation time", name = "createTime")
    private Long createTime;
    /**
     * Modification time
     */
    @Field
    @LastModifiedDate
    @ApiModelProperty(value = "Modification time", name = "updateTime")
    private Date updateTime;
}
./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/model/entity/BlogEntity.java
package com.ljq.demo.springboot.mongodb.model.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

/**
 * @Description: Blog information
 * @Author: junqiang.lu
 * @Date: 2021/11/13
 */
@Data
@ToString(callSuper = true)
@Document(value = "blog")
@ApiModel(value = "Blog information", description = "Blog information")
public class BlogEntity extends BaseEntity {

    private static final long serialVersionUID = -2124422309475024490L;

    /**
     * title
     */
    @Field
    @ApiModelProperty(value = "title", name = "title")
    private String title;
    /**
     * author
     */
    @Field
    @ApiModelProperty(value = "author", name = "author")
    private String author;
    /**
     * content
     */
    @Field
    @ApiModelProperty(value = "content", name = "content")
    private String content;
    /**
     * Number of readings
     */
    @Field
    @ApiModelProperty(value = "Number of readings", name = "countRead")
    private Integer countRead;
    /**
     * Number of likes
     */
    @Field
    @ApiModelProperty(value = "Number of likes", name = "countLike")
    private Integer countLike;
    /**
     * Client timestamp (accurate to seconds)
     */
    @Field
    @ApiModelProperty(value = "Client timestamp(Accurate to seconds)", name = "clientTimestamp")
    private Integer clientTimestamp;



}

8.2 statistical results

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/model/vo/BlogSummaryVo.java
package com.ljq.demo.springboot.mongodb.model.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @Description: Blog statistics
 * @Author: junqiang.lu
 * @Date: 2021/11/15
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value = "Blog statistics", description = "Blog statistics")
public class BlogSummaryVo implements Serializable {

    private static final long serialVersionUID = 6350918498981106620L;

    /**
     * author
     */
    @ApiModelProperty(value = "author", name = "author")
    private String author;
    /**
     * Total number of blogs
     */
    @ApiModelProperty(value = "Total number of blogs", name = "totalBlog")
    private Long totalBlog;
    /**
     * Total reading
     */
    @ApiModelProperty(value = "Total reading", name = "totalRead")
    private Long totalRead;
    /**
     * Average reading
     */
    @ApiModelProperty(value = "Average reading", name = "aveRead")
    private Long aveRead;
    /**
     * Total likes
     */
    @ApiModelProperty(value = "Total likes", name = "totalLike")
    private Long totalLike;
    /**
     * Average number of likes
     */
    @ApiModelProperty(value = "Average number of likes", name = "aveLike")
    private Long aveLike;
    /**
     * date
     */
    @ApiModelProperty(value = "date", name = "date")
    private String date;


}
./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/model/vo/BlogSummaryDiyVo.java
package com.ljq.demo.springboot.mongodb.model.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @Description: Blog custom time interval statistics results
 * @Author: junqiang.lu
 * @Date: 2021/11/18
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value = "Blog custom time interval statistics results", description = "Blog custom time interval statistics results")
public class BlogSummaryDiyVo extends BlogSummaryVo {

    private static final long serialVersionUID = 6688850910361408341L;

    /**
     * year
     */
    @ApiModelProperty(value = "year", name = "year")
    private String year;
    /**
     * month
     */
    @ApiModelProperty(value = "month", name = "month")
    private String month;
    /**
     * hour
     */
    @ApiModelProperty(value = "hour", name = "day")
    private String hour;
    /**
     * minute
     */
    @ApiModelProperty(value = "minute", name = "minute")
    private String minute;

}

8.3 date grouping information

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/model/vo/BlogGroupDateVo.java
package com.ljq.demo.springboot.mongodb.model.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * @Description: Blog date grouping information
 * @Author: junqiang.lu
 * @Date: 2021/11/19
 */
@Data
@ApiModel(value = "Blog date grouping information", description = "Blog date grouping information")
public class BlogGroupDateVo implements Serializable {

    private static final long serialVersionUID = -4828325421504898453L;

    /**
     * Date grouping field
     */
    @ApiModelProperty(value = "Date grouping field", name = "dateGroupField")
    private String dateGroupField;
    /**
     * Date format
     */
    @ApiModelProperty(value = "Date format", name = "dateFormat")
    private String dateFormat;



}

8.4 method of obtaining date grouping information

./demo-mongodb/src/main/java/com/ljq/demo/springboot/mongodb/service/impl/BlogServiceImpl.java
    /**
     * Get blog date grouping information
     *
     * @param dateType
     * @return
     */
    private BlogGroupDateVo getGroupDate(Integer dateType) {
        BlogGroupDateVo groupDateVo = new BlogGroupDateVo();
        switch (dateType) {
            case BlogConst.DATE_TYPE_DAY:
                groupDateVo.setDateGroupField("day");
                groupDateVo.setDateFormat("%Y-%m-%d");
                break;
            case BlogConst.DATE_TYPE_MONTH:
                groupDateVo.setDateGroupField("month");
                groupDateVo.setDateFormat("%Y-%m");
                break;
            default:
                groupDateVo.setDateGroupField("day");
                groupDateVo.setDateFormat("%Y-%m-%d");
                break;
        }
        return groupDateVo;
    }

9 recommended references

Aggregate query using mongoTemplate

springboot uses MongoTemplate to group statistics

How can I query the MongoTemplate of SpringBoot

java uses mongoTemplate to group and aggregate by month (non grouping fields are required to be returned)_ deelness blog - Programmer's Homestead

Group by time interval spring-data-mongo

Usage Summary of MongoDB time format conversion and time period aggregation statistics

spring mongodb fuzzy query

Mongodb series - spring data mongodb uses MongoTemplate to realize paging query

Remember to use the Aggregation class of mongoTemplate once for grouping and paging operations

mongodb advanced aggregate query

mongoDB's processing of time ISODate differs from our time zone by 8 hours

5 Github source code

Gtihub source address: https://github.com/Flying9001/springBootDemo

The official account number: 404Code, sharing half of the technology and thinking of Internet people, and interested in it.

Posted by nz_mitch on Fri, 19 Nov 2021 02:33:00 -0800