目录

事前准备

思考 1 如何解决 mysql 强关联性问题

思考 2 如何实现 dao 层 mysql 和 mongodb 的动态切换

思考 3 如何设计实现对原来的 service 无侵入式修改

思考 4 如何高效实现条件查询和数据更新

参考文档

1. Criteria 类:构建查询条件

2. Query 类:组装查询操作

3. Update 类:定义数据更新操作

三者关系总结


事前准备

  • 实体类单独封装

封装的是涉及到的三张表

  • 二次封装 MongdbTemple

这仅仅是个人的习惯单纯不喜欢直接在 service 里面调用MongdbTemple

思考 1 如何解决 mysql 强关联性问题

MongDB 数据是弱关联的

没有外键这个概念

MongoDB 这种 NoSQL 数据库在处理多表关联(article、category、tag)时没有 MySQL 那么方便。

我明白你的意思,你是担心 MongoDB 这种 NoSQL 数据库在处理多表关联(article、category、tag)时没有 MySQL 那么方便。
其实 MongoDB 处理关联数据有两种主要思路,你可以根据查询需求来选择:


1. 嵌入式文档(最终采用)

适合场景:

分类(Category)数据量不大,每个分类信息基本不变

文章详情页经常需要显示分类名

标签(Tag)数量不多,且常用

实现方式:

@Document(collection = "articles")
public class Article {
    @Id
    private String id;
    private String title;
    private String content;
    
    @Field("category")
    private Category category;  // 直接内嵌分类对象
    
    @Field("tags")
    private List<Tag> tags;     // 内嵌标签对象列表
    
    // ...其他字段
}

优点:

一次查询就能获取文章+分类+标签,无需多次查询 œ

读性能高,减少 join

无需维护外键关系

缺点:

如果分类或标签信息更新,需要更新所有引用它们的文章

标签或分类数据量大时,会造成文章文档过大


2. 引用式(类似外键,适合“多”的一方数据量大)

适合场景:

标签数量非常多,且经常被复用

分类信息经常变更

需要独立管理标签和分类

实现方式:

@Document(collection = "articles")
public class Article {
    @Id
    private String id;
    private String title;
    private String content;
    
    @Field("category_id")
    private ObjectId categoryId;  // 只存分类ID
    
    @Field("tag_ids")
    private List<ObjectId> tagIds; // 存标签ID列表
    
    // ...其他字段
}

优点:

数据冗余少

分类/标签更新只需改一处

适合大数据量的关联

缺点:

查询文章详情时需要多次查询(或用 MongoDB 的 $lookup 聚合)

代码中需要手动处理关联数据组装


3. 混合方案(推荐)

结合两种方式的优点:

文章中内嵌分类信息(因为分类变动少,且查询频繁)

文章中引用标签ID(因为标签可能频繁变更,且复用率高)

@Document(collection = "articles")
public class Article {
    @Id
    private String id;
    private String title;
    
    @Field("category")
    private Category category;  // 内嵌分类
    
    @Field("tag_ids")
    private List<ObjectId> tagIds; // 引用标签
    
    // ...
}

4. 实际操作示例

假设用引用式存储标签:

// 保存文章时
public void addArticle(ArticleReq article) {
    // 保存分类(单独集合)
    ObjectId categoryId = saveCategory(article.getCategoryName());
    
    // 保存标签(单独集合)
    List<ObjectId> tagIds = saveTags(article.getTagNameList());
    
    // 保存文章
    Article newArticle = new Article();
    newArticle.setTitle(article.getTitle());
    newArticle.setContent(article.getContent());
    newArticle.setCategoryId(categoryId);
    newArticle.setTagIds(tagIds);
    
    articleRepository.save(newArticle);
}

// 查询文章详情时组装标签
public ArticleResp getArticleById(String id) {
    Article article = articleRepository.findById(id).orElse(null);
    if (article == null) return null;
    
    ArticleResp resp = BeanCopyUtils.copyBean(article, ArticleResp.class);
    
    // 查询分类
    Category category = categoryRepository.findById(article.getCategoryId()).orElse(null);
    resp.setCategoryName(category.getCategoryName());
    
    // 查询标签
    List<Tag> tags = tagRepository.findAllById(article.getTagIds());
    resp.setTags(tags.stream().map(Tag::getTagName).collect(Collectors.toList()));
    
    return resp;
}

5. MongoDB 聚合查询(替代 SQL Join)

如果用引用式存储,可以用聚合管道一次性查询:

Aggregation agg = Aggregation.newAggregation(
    Aggregation.match(Criteria.where("_id").is(new ObjectId(articleId))),
    Aggregation.lookup("categories", "category_id", "_id", "category"),
    Aggregation.unwind("category"),
    Aggregation.lookup("tags", "tag_ids", "_id", "tags")
);

AggregationResults<ArticleDetailDTO> results = mongoTemplate.aggregate(
    agg, "articles", ArticleDetailDTO.class);
    
ArticleDetailDTO article = results.getUniqueMappedResult();

思考 2 如何实现 dao 层 mysql 和 mongodb 的动态切换

这个问题的关键在于我们要尽可能减少 原来 service 层代码的书写

我们可以在添加博客文章的时候在 mongodb 里面也存一份

可以先将代码改造成双写,再逐步迁移到 MongDB

开关在配置文件里面

我们采用的是工厂方法的设计模式

通过加载配置来引导 controller 加载不同的 service

具体代码逻辑

我们将配置写在 application.yml 配置文件里面

# 数据源, 可选值: mysql, mongodb
datatype:
#  type: mysql
  type: mongodb

DatabaseProperties 类 用于加载挂载 yml 文件里的配置

@ ConfigurationProperties 注解

package com.ican.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 数据库配置属性
 * @author Dduo
 */
@Data
@Component
@ConfigurationProperties(prefix = "datatype")
public class DatabaseProperties {
    
    /**
     * 数据库类型
     */
    private String type = "mysql";
    
    /**
     * MongoDB配置
     */
    private MongoDbProperties mongodb = new MongoDbProperties();
    
    /**
     * 是否启用MongoDB
     */
    public boolean isMongoDbEnabled() {
        return "mongodb".equalsIgnoreCase(type) || mongodb.isEnabled();
    }
    
    @Data
    public static class MongoDbProperties {
        
        /**
         * 是否启用MongoDB
         */
        private boolean enabled = false;
        
        /**
         * MongoDB集合前缀
         */
        private String collectionPrefix = "blog_";
        
        /**
         * 是否启用自动同步
         */
        private boolean autoSync = true;
        
        /**
         * 同步批次大小
         */
        private int syncBatchSize = 100;
    }
}

绑定的是具体配置内的内容

简单工厂模式

DatabaseServiceFactory 类

从 DatabaseProperties 加载配置,来决定注入的是哪个 servcie 类

package com.ican.factory;

import com.ican.config.properties.DatabaseProperties;
import com.ican.service.ArticleMongoService;
import com.ican.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 数据库服务工厂
 * @author Dduo
 */
@Component
public class DatabaseServiceFactory {
    
    @Autowired
    private DatabaseProperties databaseProperties;
    
    @Autowired
    private ArticleService articleService;
    
    @Autowired(required = false)
    private ArticleMongoService articleMongoService;
    
    /**
     * 获取文章服务
     */
    public ArticleService getArticleService() {
        if (databaseProperties.isMongoDbEnabled() && articleMongoService != null) {
            return articleMongoService;
        }
        return articleService;
    }
    
    /**
     * 获取当前数据库类型
     */
    public String getCurrentDatabaseType() {
        return databaseProperties.isMongoDbEnabled() ? "mongodb" : "mysql";
    }
}

思考 3 如何设计实现对原来的 service 无侵入式修改

在启动 nosql 后,我们无非是用于进行数据双写

先写入 mysql 再写入 mongodb

原来我们在 controller 调用的是 ArticleService 类

我们现在创建他的子类

这样可以无侵入式 进行代码修改

比如说这个 添加博客

我们子类的这个方法是继承父类而来

所以我先调用 super 里面的方法

然后在进行自己的逻辑

在 controller 里面

我们使用数据源工厂 (DatabaseServiceFactory) 在 依赖注入完成后初始化 ArticleService

思考 4 如何高效实现条件查询和数据更新

这个是全新的知识点

查询我们 使用的是一个 Criteria 类 和 Query

   /**
     * 根据ArticleQuery构建MongoDB查询条件
     */
    public Query buildArticleQuery(ArticleQuery articleQuery) {
        Criteria criteria = new Criteria();
        // 搜索内容(标题和内容包含关键字)
        if (articleQuery.getKeyword() != null && !articleQuery.getKeyword().trim().isEmpty()) {
            String keyword = articleQuery.getKeyword().trim();
            criteria.orOperator(
                    Criteria.where("title").regex(keyword, "i"),  // 标题模糊匹配,不区分大小写
                    Criteria.where("content").regex(keyword, "i") // 内容模糊匹配,不区分大小写
            );
        }
        // 分类ID匹配
        if (articleQuery.getCategoryId() != null) {
            criteria.and("categoryId").is(articleQuery.getCategoryId());
        }
        // 标签ID匹配(假设文章中存储标签ID集合)
        if (articleQuery.getTagId() != null) {
            criteria.and("tagIds").in(articleQuery.getTagId());
        }

        // 是否删除状态匹配
        if (articleQuery.getIsDelete() != null) {
            criteria.and("isDelete").is(articleQuery.getIsDelete());
        }
        // 文章状态匹配
        if (articleQuery.getStatus() != null) {
            criteria.and("status").is(articleQuery.getStatus());
        }
        // 文章类型匹配
        if (articleQuery.getArticleType() != null) {
            criteria.and("articleType").is(articleQuery.getArticleType());
        }
        // 创建查询对象
        Query query = new Query(criteria);
        // 处理分页:PageQuery的getCurrent()已返回skip值,getSize()返回每页条数
        query.skip(articleQuery.getCurrent())  // 直接使用计算好的跳过记录数
                .limit(articleQuery.getSize());   // 使用每页显示条数
        return query;
    }

数据更新是一个 Updata 类

    /**
     * 更新文章(更新mongodb中的文档内容)
     */
    @Override
    public void updateArticle(ArticleReq articleReq) {
        // 转换为文档对象
        ArticleDocument articleDocument = new ArticleDocument();
        BeanUtil.copyProperties(articleReq, articleDocument);
        // 更新文档
        Update update = new Update();
        update.set("title", articleDocument.getArticleTitle());
        update.set("content", articleDocument.getArticleContent());
        update.set("tagNameList", articleDocument.getTagNameList());
        update.set("categoryId", articleDocument.getCategoryId());
        update.set("updateTime", LocalDateTime.now());
        mongodbService.updateDocument(articleReq.getId().toString(), "article", update);
    }

参考文档

在 Java 操作 MongoDB 的过程中,CriteriaQueryUpdate 是三个核心类,分别用于构建查询条件、组装查询操作和定义数据更新操作。下面分别介绍它们的作用和使用方式:

1. Criteria 类:构建查询条件

Criteria 类用于定义具体的查询条件(如等于、大于、模糊匹配等),是构建查询的基础。它提供了一系列静态方法和实例方法来生成各种条件。

常用方法示例

  • 等于:Criteria.where("字段名").is(值)
  • 不等于:Criteria.where("字段名").ne(值)
  • 大于/小于:gt(值) / lt(值)
  • 模糊匹配:regex(正则表达式)
  • 包含:in(数组)
  • 逻辑组合:and()or()

示例

// 查询 age 大于 18 且 status 为 "active" 的文档
Criteria criteria = Criteria.where("age").gt(18)
                          .and("status").is("active");

2. Query 类:组装查询操作

Query 类用于封装查询条件(通过 Criteria),并可以设置查询的附加参数(如分页、排序、字段过滤等)。它是执行查询的载体。

常用方法

  • 绑定条件:Query.query(Criteria criteria)
  • 分页:skip(跳过条数).limit(查询条数)
  • 排序:with(Sort sort)
  • 字段过滤:fields().include("字段").exclude("字段")

示例

// 基于上面的 criteria 构建查询,并设置分页和排序
Query query = Query.query(criteria)
                   .skip(0)    // 跳过 0 条
                   .limit(10)  // 最多返回 10 条
                   .with(Sort.by(Sort.Direction.DESC, "age"));  // 按 age 降序

执行查询时,将 Query 对象传入 MongoDB 模板的方法(如 find()):

List<User> users = mongoTemplate.find(query, User.class, "collectionName");

3. Update 类:定义数据更新操作

Update 类用于描述对文档的更新动作(如修改字段值、新增字段、删除字段等)。

常用方法

  • 设置字段值:set("字段名", 值)
  • 递增/递减:inc("字段名", 数值)
  • 移除字段:unset("字段名")
  • 数组操作:push("数组字段", 元素)(添加元素)、pull("数组字段", 条件)(删除元素)

示例

// 将 status 改为 "inactive",并将 loginCount 递增 1
Update update = new Update()
    .set("status", "inactive")
    .inc("loginCount", 1);

执行更新时,结合查询条件和更新操作:

// 更新满足 query 条件的文档
UpdateResult result = mongoTemplate.updateMulti(query, update, "collectionName");

三者关系总结

  1. Criteria 负责定义查询条件(如“age > 18”);
  2. Query 负责封装查询条件和附加参数(如分页、排序),作为查询的“载体”;
  3. Update 负责定义更新动作(如“set status = 'inactive'”);
  4. 实际操作时,通过 MongoDB 模板(MongoTemplate)将 QueryUpdate 结合,执行查询或更新操作。

这种设计让查询和更新的逻辑更清晰,便于构建复杂的数据库操作。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐