Skip to content

SpringBoot + MongoDB

一、MongoDB 基本概念

MongoDB 是一个基于 文档模型(Document Model) 的数据库,它的数据以类似 JSON 的 BSON 格式存储。

MongoDB + SpringBoot 3.x 的开发效率非常高,尤其适合:

  • • IoT 设备数据
  • • 用户系统
  • • 日志/监控平台
  • • 内容管理系统
  • • 电商类复杂字段数据

与MySQL的对比关系

MongoDBMySQL含义
databasedatabase数据库
collectiontable集合(表)
documentrow文档(行)
fieldcolumn字段(列)
_idprimary key唯一主键(自动生成 ObjectId)

一个典型的 MongoDB 文档

json
{
  "_id": "672bcd94d12345a98efaa032",
  "name": "张三",
  "age": 26,
  "tags": ["java", "iot"],
  "address": {
    "city": "上海",
    "district": "浦东新区"
  }
}

二、SpringBoot3 集成 MongoDB

1.添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

2.配置连接

yaml
spring:
  data:
    mongodb:
      # 无账号密码添加
      uri: mongodb://localhost:27017/test
      # 有账号密码添加
      # url: mongodb://user:password@localhost:27017/test

3.实体类(Document)定义

java
@Data
@Schema(description = "用户表")
public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 7478513151262531440L;

    @Schema(description = "id")
    @NotBlank(groups = {UpdateGroup.class})
    private String id;

    @NotBlank(groups = {SaveGroup.class,UpdateGroup.class})
    @Schema(description = "用户名称")
    private String name;

    @NotBlank(groups = {SaveGroup.class,UpdateGroup.class})
    @Schema(description = "用户年龄")
    private Integer age;
}

4. 核心 CRUD 实战

增删改查

java
User saveUser(User user);

boolean updateUser(User user);

boolean deleteUser(String id);

List<User> findUserById(String id);

List<User> findUserByName(String name);

分页: 因为MongoDB本身不原生支持分页,只能通过跳页的方式,自定义实现分页(MongoDB深度分页效率极低)

java
//分页
PageResult<User> findUserByPage(Integer current, Integer size, String name);
//分页
PageResult<UserVO> findUserVOByPage(Integer current, Integer size);

构建分页结果类

java
@Data
@Schema(description = "分页结果")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PageResult<T> {

    @Schema(description = "页码,从1开始")
    private Integer pageNum;

    @Schema(description = "页面大小")
    private Integer pageSize;

    @Schema(description = "总数")
    private Long total;

    @Schema(description = "总页数")
    private Integer pages;

    @Schema(description = "数据")
    private List<T> list;
}

分页工具

java
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MongoPageHelper {

    /**
     * 起始页号
     */
    private static final int FIRST_PAGE_NUM = 1;
    /**
     * mongodb中的id字段
     */
    private static final String ID = "_id";

    /**
     * 分页查询,直接返回集合类型的结果
     */
    public static <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageNum, Integer pageSize) {
        return pageQuery(query, entityClass, Function.identity(), pageNum, pageSize, null);
    }

    /**
     * 分页查询,不考虑条件分页,直接使用skip-limit来分页
     */
    public static <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass, Function<T, R> mapper,
                                          Integer pageNum, Integer pageSize) {
        return pageQuery(query, entityClass, mapper, pageNum, pageSize, null);
    }

    /**
     * 分页查询 采用find(_id>lastId).limit分页
     */
    public static <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageNum, Integer pageSize, String lastId) {
        return pageQuery(query, entityClass, Function.identity(), pageNum, pageSize, lastId);
    }

    /**
     * 分页查询
     *
     * @param query Mongo Query对象,构造你自己的查询条件.
     * @param entityClass Mongo collection定义的entity class,用来确定查询哪个集合.
     * @param mapper 映射器,你从db查出来的list的元素类型是entityClass, 如果你想要转换成另一个对象,比如去掉敏感字段等,可以使用mapper来决定如何转换.
     * @param pageNum 当前页.
     * @param pageSize 分页的大小.
     * @param lastId 条件分页参数, 区别于skip-limit,采用find(_id>lastId).limit分页.
     * 如果不跳页,像朋友圈,微博这样下拉刷新的分页需求,需要传递上一页的最后一条记录的ObjectId。 如果是null,则返回pageNum那一页.
     * @param <T> collection定义的class类型.
     * @param <R> 最终返回时,展现给页面时的一条记录的类型。
     * @return PageResult,一个封装page信息的对象.
     *
     * @see MongoPageHelper#pageQuery(org.springframework.data.mongodb.core.query.Query,java.lang.Class,
     * java.util.function.Function,java.lang.Integer,java.lang.Integer,java.lang.String)
     *
     */
    public static <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass, Function<T, R> mapper,
                                          Integer pageNum, Integer pageSize, String lastId) {
        MongoTemplate mongoTemplate = ApplicationContextUtils.getBean(MongoTemplate.class);
        //分页逻辑
        long total = mongoTemplate.count(query, entityClass);
        final Integer pages = (int) Math.ceil(total / (double) pageSize);
        if (pageNum <= 0 || pageNum > pages) {
            pageNum = FIRST_PAGE_NUM;
        }
        final Criteria criteria = new Criteria();
        if (StringUtils.isNotBlank(lastId)) {
            if (pageNum != FIRST_PAGE_NUM) {
                criteria.and(ID).gt(new ObjectId(lastId));
            }
            query.limit(pageSize);
        } else {
            int skip = pageSize * (pageNum - 1);
            query.skip(skip).limit(pageSize);
        }

        final List<T> entityList = mongoTemplate
                .find(query.addCriteria(criteria)
                                .with(Sort.by(Order.asc(ID))),
                        entityClass);

        final PageResult<R> pageResult = new PageResult<>();
        pageResult.setTotal(total);
        pageResult.setPages(pages);
        pageResult.setPageSize(pageSize);
        pageResult.setPageNum(pageNum);
        pageResult.setList(entityList.stream().map(mapper).toList());
        return pageResult;
    }
}

接口实现类

java
@Slf4j
@Service
@RequiredArgsConstructor
public class MongodbServiceImpl implements MongodbService {

    private final MongoTemplate mongoTemplate;

    @Override
    public User saveUser(User user) {
        return mongoTemplate.save(user);
    }

    @Override
    public boolean updateUser(User user) {
        List<User> users = mongoTemplate.find(new Query(Criteria.where("_id").is(user.getId())), User.class);
        if (users.isEmpty()){
            throw new RuntimeException("用户不存在");
        }
        UpdateResult updateResult = mongoTemplate.updateFirst(
                new Query(Criteria.where("_id").is(user.getId())),
                new Update()
                        .set("name",user.getName())
                        .set("age",user.getAge()),
                User.class);
        return updateResult.wasAcknowledged();
    }

    @Override
    public boolean deleteUser(String id) {
        DeleteResult deleteResult = mongoTemplate.remove(new Query(Criteria.where("_id").is(id)), User.class);
        return deleteResult.wasAcknowledged();
    }

    @Override
    public List<User> findUserById(String id) {
        return mongoTemplate.find(new Query(Criteria.where("_id").is(id)),User.class);
    }

    @Override
    public List<User> findUserByName(String name) {
        return mongoTemplate.find(new Query(Criteria.where("name").is(name)),User.class);
    }

    @Override
    public PageResult<User> findUserByPage(Integer current, Integer size, String name) {
        Query query = new Query();
        if (StringUtils.isNotBlank(name)){
            query.addCriteria(Criteria.where("name").is(name));
        }
        return MongoPageHelper.pageQuery(query, User.class, current, size);
    }

    @Override
    public PageResult<UserVO> findUserVOByPage(Integer current, Integer size) {
        return MongoPageHelper.pageQuery(
                new Query(),
                User.class,
                user -> {
                    UserVO userVO = new UserVO();
                    userVO.setName(user.getName());
                    userVO.setAge(user.getAge());
                    return userVO;
                } ,
                current, size);
    }
}

5. 索引(性能优化关键)

给 name 字段创建索引

java
@Indexed
private String name;

复合索引:运行项目后索引自动创建

java
@CompoundIndex(def = "{'name':1, 'age':-1}")

6. MongoDB 事务(SpringBoot 实现)

MongoDB 自 4.0 起支持多文档事务,Spring 使用 @Transactional 即可。

注意:事务仅在 ReplicaSet(副本集)模式下生效

java
@Transactional
public void transfer() {
    mongoTemplate.save(a);
    mongoTemplate.save(b);
}