当心!Spring Data JPA 中的数据获取误区与最佳实践
你听说过 findAll()
的反模式吗?如果没有,这篇文章非常适合你。即使你听说过,看到几个实际例子也会有所帮助。
findAll()
的反模式指的是从数据库表中获取所有数据,然后对其进行处理,以便只选择必要的数据。直接获取所需的数据不是更好吗?我们将对此进行探讨!
在本文中,我将展示三种从数据库获取数据的方法的性能比较,以突出 findAll()
反模式的糟糕之处。
准备测试场景
为了展示性能比较,我将使用三种使用 Spring Data JPA 获取数据的方法:
使用
stream()
获取所有数据,并对该流进行操作以获得所需数据。使用原生查询获取数据。
使用 JPQL 查询获取数据。
以上三种方法将应用于三个不同的数据库表:
USERS — 包含10列和300行的大表
CARS — 包含5列和100行的中等表
ANIMALS — 包含3列和5行的小表
这些表之间没有关联,主键为 Long 类型,其余列为 String 或 Boolean。此次比较选择的关系型数据库是 MySQL。
对于每种获取数据的方法,结果将来自100次连续执行的汇总。对于每个数据库表,所有三种获取方法返回相同的数据。现在,我们已经了解了设置,可以进行每个测试用例。
案例 1 — USERS
对于这个案例,我使用了 USERS 表。它在列数和行数上都是最大的。使用的三种方法是:
// stream
public List<Long> getAllUserIdsStream() {
return userRepository.findAll()
.stream()
.filter(User::getActive)
.map(User::getId)
.toList();
}
// native query
@Query(
nativeQuery = true,
value = "select u.id from users where u.active is true")
List<Long> getAllUserIdsNative();
// JPQL query
@Query("select u.id from User u where u.active = true")
List<Long> getAllUserIdsJPQL();
这三种方法都在做同一件事:获取所有活跃用户的 ID。结果如下(时间以毫秒计):
USERS 性能比较(毫秒)— 大型数据库表
使用原生或 JPQL 查询时,相较于 stream()
选项有显著改善。stream()
版本平均耗时14毫秒,而原生和 JPQL 方法的平均时间仅为6毫秒,速度超过两倍。
案例 2 — CARS
第二个测试在中型表上进行,列数和行数较少。我使用了不同大小的数据库表以获得更广泛的比较。
// stream
public List<String> getCarPlatesStream() {
return carRepository.findAll()
.stream()
.map(Car::getPlate)
.toList();
}
// native
@Query(
nativeQuery = true,
value = "select c.plate from cars c")
List<String> getCarPlatesNative();
// JPQL
@Query("select c.id from Car c")
List<String> getCarPlatesJPQL();
这三种方法返回所有汽车的车牌列表。现在,让我们看看结果是否与第一个案例不同。
CARS 性能比较(毫秒)— 中型数据库表
结果与前一个示例相似,stream()
方法表现最差,而其他两种方法明显更好。这次,各方法的平均时间差异较小,因为数据库表也较小。此外,这次只有对流应用了 map()
方法,而前一个场景还进行了 filter()
,这增加了时间差异。stream()
方法平均耗时10毫秒,而另外两种方法仅耗时5毫秒。
案例 3 — ANIMALS
在这个案例中,我想检查即使表非常小且操作仅是排序方法,是否仍然存在时间差异。
// stream
public List<Animal> getAnimalsSortedStream() {
return animalRepository.findAll()
.stream()
.sorted(Comparator.comparing(Animal::getColor, String.CASE_INSENSITIVE_ORDER))
.toList();
}
// native
@Query(
nativeQuery = true,
value = "select * from animals order by upper(color)")
List<Animal> getAnimalsSortedNative();
// JPQL
@Query("select a from Animal a order by upper(a.color)")
List<Animal> getAnimalsSortedJPQL();
这三种方法返回按颜色排序的动物列表。结果如下:
ANIMALS 性能比较 — 小型数据库表
在这种情况下,差异较小,但值得注意的是,即使数据库表非常小,使用 JPQL 或原生查询获取数据仍比使用 stream()
方法更有效。在这种情况下,即使是 JPQL(耗时4毫秒)和原生查询(耗时5毫秒)之间的差异也很小。stream()
版本平均耗时7毫秒。
结论
在本文中,我们看到了三种从数据库获取数据的方法,突出了使用 findAll()
方法从数据库表获取所有数据并对其进行处理的低效性,而不是直接获取必要的数据。
三个表(USERS, CARS 和 ANIMALS)的性能比较
如果 findAll()
反模式在上述三个简单表上的表现如此糟糕,想象一下在可能有成千上万行的生产数据库中情况会多糟糕。与其他表的关联可能会使情况更糟。
总之,请务必记住,绝不要在 Spring Data JPA 的 findAll()
方法上执行 stream()
操作,因为这非常低效。