1. 概述

本文将介绍如何使用Java和MongoDB实现一个简单的标签系统。

标签(Tag)本质上是用于文档分类的"关键词标签"。通过标签,用户可以快速浏览相似内容,这在处理海量数据时特别实用。

这种技术在博客系统中尤为常见:每篇文章根据主题分配一个或多个标签,用户阅读后可通过标签跳转到相关主题的更多内容。下面我们来看看具体实现方案。

2. 依赖配置

首先在pom.xml中添加MongoDB Java驱动依赖:

<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongo-java-driver</artifactId>
    <version>3.6.3</version>
</dependency>

最新版本可在Maven中央仓库查询。

3. 数据模型设计

我们先定义博文文档的基本结构。为简化示例,模型仅包含:

  • 标题(同时作为文档ID)
  • 作者
  • 标签数组(支持多标签)
{
    "_id" : "Java 8 and MongoDB",
    "author" : "Donato Rimenti",
    "tags" : ["Java", "MongoDB", "Java 8", "Stream API"]
}

对应的Java实体类:

public class Post {
    private String title;
    private String author;
    private List<String> tags;

    // getters and setters
}

4. 标签更新操作

现在实现标签的增删操作。仓库类提供两个核心方法:

  • 通过标题定位文档
  • 返回布尔值表示操作是否成功
public boolean addTags(String title, List<String> tags) {
    UpdateResult result = collection.updateOne(
      new BasicDBObject(DBCollection.ID_FIELD_NAME, title), 
      Updates.addEachToSet(TAGS_FIELD, tags));
    return result.getModifiedCount() == 1;
}

public boolean removeTags(String title, List<String> tags) {
    UpdateResult result = collection.updateOne(
      new BasicDBObject(DBCollection.ID_FIELD_NAME, title), 
      Updates.pullAll(TAGS_FIELD, tags));
    return result.getModifiedCount() == 1;
}

⚠️ 踩坑提醒:添加标签时使用addEachToSet而非push,可避免重复添加已存在的标签。addToSet操作符也不适用,它会创建嵌套数组结构。

Shell操作示例:更新标题为"JUnit 5 with Java"的博文

// 添加标签
db.posts.updateOne(
    { _id : "JUnit 5 with Java" }, 
    { $addToSet : 
        { "tags" : 
            { $each : ["Java", "JUnit5"] }
        }
});

// 删除标签
db.posts.updateOne(
    {_id : "JUnit 5 with Java" },
    { $pull : 
        { "tags" : { $in : ["Spring", "REST"] }
    }
});

5. 标签查询实现

标签系统最常用的三类查询场景:

操作符 功能说明
$in 返回包含任意指定标签的文档
$nin 返回不包含任何指定标签的文档
$all 返回包含所有指定标签的文档

Java实现:使用Stream API处理文档转换

public List<Post> postsWithAtLeastOneTag(String... tags) {
    FindIterable<Document> results = collection
      .find(Filters.in(TAGS_FIELD, tags));
    return StreamSupport.stream(results.spliterator(), false)
      .map(TagRepository::documentToPost)
      .collect(Collectors.toList());
}

public List<Post> postsWithAllTags(String... tags) {
    FindIterable<Document> results = collection
      .find(Filters.all(TAGS_FIELD, tags));
    return StreamSupport.stream(results.spliterator(), false)
      .map(TagRepository::documentToPost)
      .collect(Collectors.toList());
}

public List<Post> postsWithoutTags(String... tags) {
    FindIterable<Document> results = collection
      .find(Filters.nin(TAGS_FIELD, tags));
    return StreamSupport.stream(results.spliterator(), false)
      .map(TagRepository::documentToPost)
      .collect(Collectors.toList());
}

private static Post documentToPost(Document document) {
    Post post = new Post();
    post.setTitle(document.getString(DBCollection.ID_FIELD_NAME));
    post.setAuthor(document.getString("author"));
    post.setTags((List<String>) document.get(TAGS_FIELD));
    return post;
}

Shell查询示例

// 查询含MongoDB或Stream API标签的文章
db.posts.find({
    "tags" : { $in : ["MongoDB", "Stream API" ] } 
});

// 查询同时含Java 8和JUnit 5标签的文章
db.posts.find({
    "tags" : { $all : ["Java 8", "JUnit 5" ] } 
});

// 查询不含Groovy或Scala标签的文章
db.posts.find({
    "tags" : { $nin : ["Groovy", "Scala" ] } 
});

6. 总结

本文展示了基于MongoDB的标签系统核心实现。这种设计不仅适用于博客系统,稍作调整即可用于其他分类场景。

关键要点

  1. 使用数组存储多标签
  2. 通过$addToSet避免重复标签
  3. 灵活运用$in/$nin/$all实现复杂查询

想深入学习MongoDB?推荐阅读这篇入门教程。完整示例代码可在Github项目获取。


原始标题:A Simple Tagging Implementation with MongoDB | Baeldung