1. 概述

标签是一种标准设计模式,用于对数据模型中的项目进行分类和过滤。

本文将使用Spring和JPA实现标签功能,主要通过Spring Data完成。如果你使用Hibernate,这个实现同样适用。

这是标签实现系列文章的第二篇,关于Elasticsearch的实现可参考这里

2. 添加标签

首先我们探索最直接的标签实现方式:字符串列表。 通过在实体类中添加新字段实现:

@Entity
public class Student {
    // ...

    @ElementCollection
    private List<String> tags = new ArrayList<>();

    // ...
}

注意新字段上的@ElementCollection注解。由于需要持久化存储,必须告诉数据库如何处理标签。

如果不加这个注解,标签会被序列化存储为一个大字段,后续操作会很麻烦。该注解会创建名为STUDENT_TAGS的关联表(即<实体名>_<字段名>),使查询更健壮。

这会在实体和标签间建立一对多关系! 这里我们实现的是最简版标签,因此可能产生大量重复标签(每个带标签的实体都会存储一份)。后续会深入讨论这个问题。

3. 构建查询

标签能让我们对数据进行有趣的操作:按特定标签搜索、过滤表扫描、限制查询结果范围。我们逐一分析这些场景。

3.1. 搜索标签

数据模型中的tags字段可以像其他字段一样被搜索。构建查询时,标签存储在独立表中。

按标签搜索实体的实现:

@Query("SELECT s FROM Student s JOIN s.tags t WHERE t = LOWER(:tag)")
List<Student> retrieveByTag(@Param("tag") String tag);

由于标签存储在独立表,查询时需要JOIN操作——这会返回所有匹配标签的Student实体。

先准备测试数据:

Student student = new Student(0, "Larry");
student.setTags(Arrays.asList("full time", "computer science"));
studentRepository.save(student);

Student student2 = new Student(1, "Curly");
student2.setTags(Arrays.asList("part time", "rocket science"));
studentRepository.save(student2);

Student student3 = new Student(2, "Moe");
student3.setTags(Arrays.asList("full time", "philosophy"));
studentRepository.save(student3);

Student student4 = new Student(3, "Shemp");
student4.setTags(Arrays.asList("part time", "mathematics"));
studentRepository.save(student4);

测试验证:

// 获取第一个结果
Student student2 = studentRepository.retrieveByTag("full time").get(0);
assertEquals("name incorrect", "Larry", student2.getName());

返回第一个带full time标签的学生,符合预期。

扩展示例展示如何过滤大数据集:

List<Student> students = studentRepository.retrieveByTag("full time");
assertEquals("size incorrect", 2, students.size());

稍加改造,就能让仓库方法接收多个标签作为过滤条件,进一步精炼结果。

3.2. 查询过滤

简单标签的另一个实用场景是给特定查询添加过滤器。虽然前例也能实现过滤,但它们作用于全表数据。

当需要过滤其他搜索时,看这个例子:

@Query("SELECT s FROM Student s JOIN s.tags t WHERE s.name = LOWER(:name) AND t = LOWER(:tag)")
List<Student> retrieveByNameFilterByTag(@Param("name") String name, @Param("tag") String tag);

这个查询与前例几乎相同,标签只是查询中的另一个约束条件。

使用示例:

Student student2 = studentRepository.retrieveByNameFilterByTag(
  "Moe", "full time").get(0);
assertEquals("name incorrect", "moe", student2.getName());

核心优势:可将标签过滤器应用于该实体的任何查询,让用户在界面中精准定位所需数据。

4. 高级标签方案

简单标签实现是很好的起点,但一对多关系会带来一些问题:

  1. ⚠️ 数据冗余:标签表中会产生大量重复条目。小项目无妨,但大型系统可能面临数百万甚至数十亿重复数据
  2. 模型局限:当前实现无法记录标签创建时间等额外信息
  3. 类型隔离:标签无法跨实体类型共享,导致更多重复数据影响性能

多对多关系能解决大部分问题。 关于@ManyToMany注解的使用可参考此文(超出本文范围)。

5. 总结

标签是查询数据的简单直接方式,结合JPA能轻松实现强大的过滤功能。

虽然简单实现并非总是最优解,但本文已指出改进方向。

本文代码可在GitHub获取。


原始标题:A Simple Tagging Implementation with JPA