使用JPA的简单标签实现

2023/05/18

1. 概述

标签是一种标准设计模式,它允许我们对数据模型中的元素进行分类和过滤。

在本文中,我们将使用Spring Data来实现标签。这是关于实现标签系列文章中的第二篇。要了解如何使用Elasticsearch实现它,请访问此处

2. 添加标签

首先,我们介绍最直接的标签实现:字符串集合。我们可以通过向实体添加一个新字段来实现标签,如下所示:

@Entity
public class Student {
    // ...

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

    // ...
}

注意tags字段上使用的@ElementCollection注解。由于这里涉及到数据库,我们需要告诉它如何存储我们的标签。

如果我们不添加该注解,它们将存储在一个单一的blob中,这会更难处理。这个注解创建另一个名为STUDENT_TAGS(即<entity>_<field>)的表,这可以使我们的查询更加健壮。实际上,这在我们的实体和标签之间创建了一对多关系,我们在这里实现了最简单的标签版本。因此,我们可能会有很多重复的标签(每个实体都有一个标签),稍后我们会详细讨论这个概念。

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);

因为标签存储在另一个表中,所以我们需要在查询中进行表连接,这将返回所有具有匹配标签的学生实体。

首先,我们添加一些测试数据:

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);

接下来,我们测试它以确保标签有效:

// Grab only the first result
Student student2 = studentRepository.retrieveByTag("full time").get(0);
assertEquals("name incorrect", "Larry", student2.getName());

我们得到的是Repository中带有“full time”标签的第一个学生,这正是我们想要的。

此外,我们可以扩展这个例子来演示如何过滤更大的数据集,例如:

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

通过一些重构,我们可以修改Repository以将多个标签作为过滤器,这样我们就可以进一步细化我们的结果。

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. 高级标签

简单的标签实现是一个很好的开始,但是,由于一对多关系,我们可能会遇到一些问题

首先,我们最终会得到一个充满重复标签的表。这对小项目来说不是问题,但大型系统最终可能会出现数百万(甚至数十亿)个重复记录。此外,我们的标签模型不是很健壮。如果我们想跟踪最初创建标签的时间怎么办?在我们目前的实现中,我们无法做到这一点。最后,我们不能在多个实体类型之间共享我们的标签。这可能会导致更多的重复,从而影响我们的系统性能。

而多对多关系的标签可以解决大部分的这些问题

5. 总结

标签是一种能够查询数据的简单直接的方法,结合Java Persistence API,我们获得了一个易于实现的强大过滤功能。虽然简单的实现可能并不总是最合适的,但我们已经强调了帮助解决这种情况的途径。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章