2026西湖龙井茶官网DTC发售:茶农直供,政府溯源防伪到农户家
每当有人发布对象关系映射(ORM)的性能基准测试时,总会出现一种争论:“当然原生Java数据库连接(JDBC)更快,你测量的是抽象层的开销”。这种观点有一定道理,但只对了一半。没人指出的是,抽象层并非唯一的罪魁祸首——有时问题出在你自己身上,因为你在不知不觉中放任了N+1查询问题的发生。
我构建了 prismavsjdbc 项目,以便在受控环境下验证这一观点。这并非一个旨在决出胜负的基准测试,而是一个实验平台。在该平台中,相同的PostgreSQL 16数据库、相同的5万条任务数据集以及相同的业务场景,分别在两种技术栈上运行:一方是Node.js 24长期支持版(LTS)+ TypeScript + Prisma 5,另一方是Spring Boot 3 + Java 21长期支持版(LTS)+ JdbcTemplate。所分析的代码提交哈希值为 2cd33e32bd29a1d4b46a26af0b56d6a912f5e4f5,标签为 best-effort-editorial-final。
我所主张的核心论点是:查询结构、每次请求的SQL数量以及N+1查询问题,比“ORM与直接SQL对比”这样的口号更能解释性能差异。当优化查询结构时,两种技术栈的性能都会提升;反之,若不加优化,两者都会付出性能代价。
那个差点导致我得出错误结论的问题
该实验平台的初始版本存在一个明显的陷阱,尽管我起初并未察觉。它将Prisma最便捷的实现方式(使用 include 加载关联数据)与JDBC中的手动连接(join)操作进行对比。结果不出所料:JDBC测得每次请求执行1条SQL语句,而惯用写法下的Prisma在 read-by-id(按ID读取)场景中测得每次请求执行4条SQL语句,延迟数据也反映了这一差异。
我差点发布的错误结论是:“Prisma速度较慢,因为它发出了更多的查询。”
正确的结论是:我当时比较的是不同的查询结构。Prisma的 include 功能会针对每个关联关系执行独立的查询——这不是缺陷,而是其应用程序接口(API)文档中明确约定的行为。而JDBC之所以执行连接操作,是因为我手动编写了相应的SQL语句。如果不承认这一前提就直接对比,是不公平的。
正是这种认知摩擦改变了整个实验平台的设计思路:我需要在每种技术栈内部划分三个层级。
三个层级: naive(基础版)、idiomatic(惯用版)、best-effort(尽力优化版)
在 results/comparison.csv 文件中增加 level(层级)列,是本项目中最重要的决策。若无此列,任何结果表格都会对读者产生误导。
- naive(基础版):尽可能直接的实现方式,不考虑性能优化。在两种技术栈中,这都包括故意制造的N+1查询问题——即在循环中为每个任务执行查询。
-
idiomatic(惯用版):在每种技术栈中以正常且可维护的方式编写代码。对于Prisma,指使用
include和_count;对于JDBC,指任何Java开发人员在不痴迷于微优化的情况下都会编写的连接(join)查询。 -
best-effort(尽力优化版):团队可接受的最精简代码,且不沦为晦涩的黑客技巧。对于Prisma,这意味着当查询结构涉及聚合操作时,降级使用
$queryRaw。
在Prisma惯用写法下的 read-by-id 场景中,由于使用了 include,测得每次请求执行4条SQL语句。而使用 $queryRaw 的 read-by-id-best-effort 变体将这一数字降低至每次请求1条SQL语句——这与JDBC使用的连接查询相同。PostgreSQL对该查询的执行计划非常清晰:
-- read-by-id-best-effort:Prisma $queryRaw 与 JdbcTemplate 使用相同的SQL
select t.id, t.title, t.status, t.created_at 免责声明:本文内容来自互联网,该文观点不代表本站观点。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请到页面底部单击反馈,一经查实,本站将立刻删除。