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. 高级标签方案
简单标签实现是很好的起点,但一对多关系会带来一些问题:
- ⚠️ 数据冗余:标签表中会产生大量重复条目。小项目无妨,但大型系统可能面临数百万甚至数十亿重复数据
- ❌ 模型局限:当前实现无法记录标签创建时间等额外信息
- ❌ 类型隔离:标签无法跨实体类型共享,导致更多重复数据影响性能
多对多关系能解决大部分问题。 关于@ManyToMany
注解的使用可参考此文(超出本文范围)。
5. 总结
标签是查询数据的简单直接方式,结合JPA能轻松实现强大的过滤功能。
虽然简单实现并非总是最优解,但本文已指出改进方向。
本文代码可在GitHub获取。