Spring Boot 整合 MongoDB 最佳实践:CRUD、分页、事务、索引全覆盖

大家好,我是小悟。

一、需求描述

业务场景:开发一个博客文章管理系统

  • 实现文章的基本CRUD操作
  • 支持文章的评论功能
  • 支持根据标题和内容模糊搜索
  • 记录文章的创建和更新时间
  • 支持文章标签管理

技术需求

  • 使用Spring Boot 2.x
  • 使用MongoDB存储数据
  • 提供RESTful API接口
  • 使用MongoTemplate和MongoRepository两种方式操作数据

二、详细步骤

1. 环境准备

安装MongoDB

bash 复制代码
# MacOS
brew install mongodb-community
brew services start mongodb-community

# 验证安装
mongo --version

2. 创建Spring Boot项目

pom.xml依赖配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>mongodb-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Data MongoDB -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3. 配置文件

application.yml

yaml 复制代码
spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: blogdb
      # 如果需要认证
      # username: admin
      # password: admin123
      # authentication-database: admin

server:
  port: 8080

logging:
  level:
    org.springframework.data.mongodb: DEBUG

4. 实体类设计

Article.java

kotlin 复制代码
package com.example.mongodb.entity;

import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.time.LocalDateTime;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "articles")  // 指定集合名称
public class Article {
    
    @Id
    private String id;  // MongoDB默认使用ObjectId,这里用String接收
    
    @Indexed(unique = true)  // 创建唯一索引
    private String title;
    
    @Field("content")  // 指定字段名
    private String content;
    
    private String author;
    
    private List<String> tags;
    
    private Integer viewCount;
    
    private List<Comment> comments;
    
    @Field("createTime")
    private LocalDateTime createTime;
    
    @Field("updateTime")
    private LocalDateTime updateTime;
    
    private Boolean published;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Comment {
        private String username;
        private String content;
        private LocalDateTime commentTime;
        private Integer likes;
    }
}

5. Repository层

使用MongoRepository方式

java 复制代码
package com.example.mongodb.repository;

import com.example.mongodb.entity.Article;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ArticleRepository extends MongoRepository<Article, String> {
    
    // 根据作者查询
    List<Article> findByAuthor(String author);
    
    // 根据标签查询
    List<Article> findByTagsIn(List<String> tags);
    
    // 根据标题模糊查询
    List<Article> findByTitleLike(String title);
    
    // 根据发布时间范围查询
    List<Article> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
    
    // 使用@Query注解自定义查询
    @Query("{'title': {$regex: ?0}, 'published': true}")
    List<Article> findPublishedArticlesByTitle(String title);
    
    // 复杂的嵌套查询
    @Query("{'comments.username': ?0}")
    List<Article> findByCommentUsername(String username);
}

使用MongoTemplate方式

kotlin 复制代码
package com.example.mongodb.repository;

import com.example.mongodb.entity.Article;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class ArticleTemplateRepository {
    
    @Autowired
    private MongoTemplate mongoTemplate;
    
    // 条件查询
    public List<Article> findByCondition(String keyword, String author) {
        Query query = new Query();
        
        // 添加查询条件
        if (keyword != null && !keyword.isEmpty()) {
            query.addCriteria(Criteria.where("title").regex(keyword)
                    .orOperator(Criteria.where("content").regex(keyword)));
        }
        
        if (author != null && !author.isEmpty()) {
            query.addCriteria(Criteria.where("author").is(author));
        }
        
        // 只查询已发布的文章
        query.addCriteria(Criteria.where("published").is(true));
        
        // 排序
        query.with(Sort.by(Sort.Direction.DESC, "createTime"));
        
        return mongoTemplate.find(query, Article.class);
    }
    
    // 更新评论点赞数
    public void updateCommentLikes(String articleId, String commentUsername, int increment) {
        Query query = new Query(Criteria.where("id").is(articleId)
                .and("comments.username").is(commentUsername));
        
        Update update = new Update().inc("comments.$.likes", increment);
        
        mongoTemplate.updateFirst(query, update, Article.class);
    }
    
    // 增加文章浏览量
    public void incrementViewCount(String articleId) {
        Query query = new Query(Criteria.where("id").is(articleId));
        Update update = new Update().inc("viewCount", 1);
        mongoTemplate.updateFirst(query, update, Article.class);
    }
    
    // 批量更新
    public void updateArticlesStatusByAuthor(String author, Boolean status) {
        Query query = new Query(Criteria.where("author").is(author));
        Update update = new Update().set("published", status);
        mongoTemplate.updateMulti(query, update, Article.class);
    }
}

6. Service层

typescript 复制代码
package com.example.mongodb.service;

import com.example.mongodb.entity.Article;
import com.example.mongodb.repository.ArticleRepository;
import com.example.mongodb.repository.ArticleTemplateRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Service
public class ArticleService {
    
    @Autowired
    private ArticleRepository articleRepository;
    
    @Autowired
    private ArticleTemplateRepository articleTemplateRepository;
    
    // 创建文章
    public Article createArticle(Article article) {
        article.setId(null); // 确保是新文章
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());
        article.setViewCount(0);
        return articleRepository.save(article);
    }
    
    // 更新文章
    public Article updateArticle(String id, Article article) {
        return articleRepository.findById(id)
                .map(existingArticle -> {
                    article.setId(id);
                    article.setCreateTime(existingArticle.getCreateTime());
                    article.setUpdateTime(LocalDateTime.now());
                    return articleRepository.save(article);
                })
                .orElseThrow(() -> new RuntimeException("文章不存在"));
    }
    
    // 删除文章
    public void deleteArticle(String id) {
        articleRepository.deleteById(id);
    }
    
    // 查询单篇文章
    public Article getArticle(String id) {
        return articleRepository.findById(id)
                .map(article -> {
                    // 阅读量+1
                    articleTemplateRepository.incrementViewCount(id);
                    article.setViewCount(article.getViewCount() + 1);
                    return article;
                })
                .orElseThrow(() -> new RuntimeException("文章不存在"));
    }
    
    // 查询所有文章
    public List<Article> getAllArticles() {
        return articleRepository.findAll();
    }
    
    // 根据作者查询
    public List<Article> getArticlesByAuthor(String author) {
        return articleRepository.findByAuthor(author);
    }
    
    // 条件搜索
    public List<Article> searchArticles(String keyword, String author) {
        return articleTemplateRepository.findByCondition(keyword, author);
    }
    
    // 添加评论
    public Article addComment(String articleId, Article.Comment comment) {
        return articleRepository.findById(articleId)
                .map(article -> {
                    comment.setCommentTime(LocalDateTime.now());
                    comment.setLikes(0);
                    article.getComments().add(comment);
                    article.setUpdateTime(LocalDateTime.now());
                    return articleRepository.save(article);
                })
                .orElseThrow(() -> new RuntimeException("文章不存在"));
    }
    
    // 点赞评论
    public void likeComment(String articleId, String username) {
        articleTemplateRepository.updateCommentLikes(articleId, username, 1);
    }
}

7. Controller层

less 复制代码
package com.example.mongodb.controller;

import com.example.mongodb.entity.Article;
import com.example.mongodb.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/articles")
public class ArticleController {
    
    @Autowired
    private ArticleService articleService;
    
    @PostMapping
    public ResponseEntity<Article> createArticle(@RequestBody Article article) {
        Article created = articleService.createArticle(article);
        return new ResponseEntity<>(created, HttpStatus.CREATED);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Article> updateArticle(@PathVariable String id, 
                                                  @RequestBody Article article) {
        Article updated = articleService.updateArticle(id, article);
        return ResponseEntity.ok(updated);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteArticle(@PathVariable String id) {
        articleService.deleteArticle(id);
        return ResponseEntity.noContent().build();
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Article> getArticle(@PathVariable String id) {
        Article article = articleService.getArticle(id);
        return ResponseEntity.ok(article);
    }
    
    @GetMapping
    public ResponseEntity<List<Article>> getAllArticles() {
        List<Article> articles = articleService.getAllArticles();
        return ResponseEntity.ok(articles);
    }
    
    @GetMapping("/search")
    public ResponseEntity<List<Article>> searchArticles(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) String author) {
        List<Article> articles = articleService.searchArticles(keyword, author);
        return ResponseEntity.ok(articles);
    }
    
    @PostMapping("/{id}/comments")
    public ResponseEntity<Article> addComment(
            @PathVariable String id,
            @RequestBody Article.Comment comment) {
        Article article = articleService.addComment(id, comment);
        return ResponseEntity.ok(article);
    }
    
    @PostMapping("/{id}/comments/{username}/like")
    public ResponseEntity<Void> likeComment(
            @PathVariable String id,
            @PathVariable String username) {
        articleService.likeComment(id, username);
        return ResponseEntity.ok().build();
    }
}

8. 启动类

kotlin 复制代码
package com.example.mongodb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@SpringBootApplication
@EnableMongoRepositories  // 启用MongoDB仓库
@EnableMongoAuditing      // 启用MongoDB审计功能
public class MongodbApplication {
    public static void main(String[] args) {
        SpringApplication.run(MongodbApplication.class, args);
    }
}

9. 测试数据初始化

java 复制代码
package com.example.mongodb.config;

import com.example.mongodb.entity.Article;
import com.example.mongodb.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

@Component
public class DataInitializer implements CommandLineRunner {
    
    @Autowired
    private ArticleRepository articleRepository;
    
    @Override
    public void run(String... args) throws Exception {
        // 清空数据
        articleRepository.deleteAll();
        
        // 创建测试文章
        Article article1 = Article.builder()
                .title("Spring Boot 入门教程")
                .content("本文将介绍Spring Boot的基本使用方法...")
                .author("张三")
                .tags(Arrays.asList("Java", "Spring Boot", "教程"))
                .viewCount(0)
                .published(true)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .comments(Arrays.asList(
                        Article.Comment.builder()
                                .username("李四")
                                .content("写的很好,学习了!")
                                .commentTime(LocalDateTime.now())
                                .likes(5)
                                .build()
                ))
                .build();
        
        Article article2 = Article.builder()
                .title("MongoDB 实战技巧")
                .content("分享MongoDB在实际项目中的应用经验...")
                .author("王五")
                .tags(Arrays.asList("数据库", "MongoDB", "实战"))
                .viewCount(0)
                .published(true)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .comments(Arrays.asList())
                .build();
        
        articleRepository.saveAll(Arrays.asList(article1, article2));
        
        System.out.println("测试数据初始化完成!");
    }
}

三、测试接口

使用curl测试API

arduino 复制代码
# 创建文章
curl -X POST http://localhost:8080/api/articles \
  -H "Content-Type: application/json" \
  -d '{
    "title": "MongoDB高级特性",
    "content": "本文将介绍MongoDB的高级特性...",
    "author": "赵六",
    "tags": ["MongoDB", "数据库", "高级"],
    "published": true
  }'

# 查询所有文章
curl http://localhost:8080/api/articles

# 搜索文章
curl "http://localhost:8080/api/articles/search?keyword=Spring&author=张三"

# 添加评论
curl -X POST http://localhost:8080/api/articles/{id}/comments \
  -H "Content-Type: application/json" \
  -d '{
    "username": "读者A",
    "content": "非常实用的教程!"
  }'

# 点赞评论
curl -X POST http://localhost:8080/api/articles/{id}/comments/读者A/like

四、详细总结

1. MongoDB与关系型数据库的对比

特性 MongoDB MySQL
数据结构 文档型(BSON/JSON) 表格(行/列)
关联查询 内嵌文档或DBRef JOIN操作
Schema 动态schema 固定schema
扩展性 原生支持水平扩展 主从复制/分库分表
事务支持 4.0后支持多文档事务 完整ACID支持

2. Spring Data MongoDB的核心注解

注解 作用
@Document 映射MongoDB的集合
@Id 标识文档ID字段
@Field 指定字段名
@Indexed 声明索引
@CompoundIndex 复合索引
@DBRef 引用其他集合(谨慎使用)
@Transactional 支持事务操作

3. 操作方式对比

MongoRepository的优点:

  • 代码简洁,继承接口即可获得基础CRUD
  • 支持方法名解析查询
  • 与Spring Data JPA使用方式相似

MongoTemplate的优点:

  • 更灵活的查询条件构造
  • 支持批量操作和聚合查询
  • 可以执行原生MongoDB命令

4. 最佳实践建议

1. 索引设计

  • 为常用查询字段创建索引
  • 复合索引要注意字段顺序
  • 使用@Indexed注解或mongoTemplate创建索引

2. 性能优化

ini 复制代码
// 分页查询示例
Pageable pageable = PageRequest.of(0, 10, Sort.by("createTime").descending());
Page<Article> page = articleRepository.findAll(pageable);

// 只返回需要的字段
Query query = new Query();
query.fields().include("title").include("author");

3. 数据一致性

scss 复制代码
@Transactional
public void updateWithTransaction(String articleId, Comment comment) {
    // 确保两个操作原子性
    addComment(articleId, comment);
    updateArticleStats(articleId);
}

4. 异常处理

kotlin 复制代码
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(DuplicateKeyException.class)
    public ResponseEntity<String> handleDuplicateKey() {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body("文章标题已存在");
    }
}

5. 常见问题解决

1. 连接认证问题

yaml 复制代码
spring:
  data:
    mongodb:
      uri: mongodb://username:password@localhost:27017/dbname?authSource=admin

2. 大数据量处理

scss 复制代码
// 使用流式处理
@Autowired
private MongoTemplate mongoTemplate;

public void processLargeData() {
    Query query = new Query();
    query.cursorBatchSize(1000); // 设置批处理大小
    
    mongoTemplate.stream(query, Article.class)
        .forEach(article -> {
            // 处理每条记录
        });
}

3. 审计功能

kotlin 复制代码
@EntityListeners(AuditingEntityListener.class)
public class Article {
    @CreatedDate
    private LocalDateTime createTime;
    
    @LastModifiedDate
    private LocalDateTime updateTime;
}

6. 项目结构建议

bash 复制代码
src/main/java/com/example/mongodb/
├── config/           # 配置类
├── entity/           # 实体类
├── repository/       # 数据访问层
├── service/          # 业务逻辑层
├── controller/       # REST控制器
├── dto/              # 数据传输对象
├── exception/        # 异常处理
└── util/             # 工具类

五、总结

通过本实战项目,我们完整地实现了Spring Boot与MongoDB的整合,涵盖了:

  1. 基础配置:Maven依赖、YAML配置
  2. 数据建模:文档设计、嵌套文档使用
  3. 数据访问:Repository和Template两种方式
  4. 业务实现:CRUD操作、复杂查询、评论功能
  5. 性能优化:索引、分页、批量处理
  6. 最佳实践:事务、审计、异常处理

MongoDB作为NoSQL数据库的代表,特别适合以下场景:

  • 数据结构不固定,需要频繁变更
  • 需要快速迭代开发
  • 读写并发高,需要水平扩展
  • 存储复杂嵌套结构的JSON数据

Spring Data MongoDB提供了简洁的API,让开发者能够专注于业务逻辑,快速构建高性能的应用。通过合理使用MongoDB的特性,可以充分发挥其文档数据库的优势。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
开心就好20253 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
省长3 小时前
Sa-Token v1.45.0 发布 🚀,正式支持 Spring Boot 4、新增 Jackson3/Snack4 插件适配
java·后端·开源
开心就好20253 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
神奇小汤圆4 小时前
牛客网Java面试题总结(金三银四最新版)
后端
NE_STOP4 小时前
MyBatis-动态sql与高级映射
java
后端AI实验室4 小时前
我把同一个需求分别交给初级程序员、高级程序员和AI,结果让我沉默了
java·ai
Cache技术分享4 小时前
346. Java IO API - 操作文件和目录
前端·后端
sTone873754 小时前
web后端开发概念: VO 和 PO
java·后端·架构