news 2026/9/11 1:38:18

PostgREST 资源表示(Resource Representation):基于 HTTP 内容协商的响应格式控制完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgREST 资源表示(Resource Representation):基于 HTTP 内容协商的响应格式控制完全指南

PostgREST 资源表示(Resource Representation):基于 HTTP 内容协商的响应格式控制完全指南

【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest

PostgREST 将 Postgres 数据库暴露为 REST API,其资源表示遵循 RFC 7231 的 HTTP 内容协商机制:同一个 API 端点,可以根据请求中Accept头的内容,以 JSON、CSV 等不同格式返回数据。本文以 resource_representation.rst 为主体,结合仓库源码(MediaType.hs、Plan/Negotiate.hs)与测试用例,系统讲解响应格式协商、内置媒体类型处理器、单数/复数资源控制、null 剥离以及请求体媒体类型,帮助读者掌握精确控制 PostgREST 输入输出格式的完整能力。

核心机制:HTTP 内容协商(RFC 7231)

PostgREST 遵循 RFC 7231 第 5.3 节规定的 HTTP 内容协商机制来交付资源表示。这意味着同一个 API 端点可以响应不同格式(如 JSON 或 CSV),具体格式由客户端请求决定,服务端无需为每种格式提供不同 URL。

在源码层面,内容协商由 Plan/Negotiate.hs 中的negotiateContent函数实现:

-- | Do content negotiation. i.e. choose a media type based on the -- intersection of accepted/produced media types. negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler

其核心逻辑是:把请求头Accept中列出的媒体类型与目标资源"能产出"的媒体类型求交集,选择第一个匹配项(firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts)。如果交集为空(Nothing),则返回MediaTypeError,对应 HTTP 415 状态码。

客户端的Accept头在 ApiRequest.hs 中被解析:

iAcceptMediaType = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"

注意两个细节:

  • 未发送Accept头时,默认值是[MTAny](即*/*);
  • 一个Accept头可以包含多个媒体类型(逗号分隔),PostgREST 会按顺序逐一尝试匹配。

响应格式(Response Format):通过 Accept 头指定

使用Accept请求头来指定响应可接受的格式(可以指定一种或多种):

curl "http://localhost:3000/people" \ -H "Accept: application/json"

关于列顺序的说明

响应的列顺序不保证与select子句中指定的顺序一致。例如,带资源嵌入(resource embedding)的请求:

http://localhost:3000/films?select=directors(last_name,id),title

返回结果可能是:

[ { "title": "title", "directors": { "id": 5, "last_name": "name" } } ]

这与 JSON Schema 规范(2020-12 版)对对象的定义一致——"object: An unordered set of properties mapping a string to an instance",即 JSON 对象是字符串到实例的无序映射。因此,依赖响应中字段顺序的客户端代码是不可靠的,应始终通过字段名访问属性。资源嵌入的完整用法参见 resource_embedding.rst。

内置媒体类型处理器(Builtin Media Type Handlers)

PostgREST 为常见标准媒体类型提供了内置处理器:

媒体类型适用端点说明
text/csvapplication/json所有 API 端点表/视图见 tables_views.rst,函数见 functions.rst
application/openapi+json根端点(/返回 OpenAPI 文档,见 openapi.rst
application/geo+json相关端点GeoJSON 格式
*/*所有端点对 API 端点解析为application/json,对根端点解析为application/openapi+json

供应商媒体类型(Vendor Media Types)

PostgREST 还支持以下供应商(vendor)媒体类型处理器:

  • application/vnd.pgrst.plan:返回执行计划(EXPLAIN),见 media_type_handlers.rst 中的计划相关章节;
  • application/vnd.pgrst.objectapplication/vnd.pgrst.array:控制单数/复数响应与 null 剥离,详见下文"单数或复数"与"剥离空值"两节。

在源码 MediaType.hs 中,这些媒体类型被建模为 Haskell 代数数据类型:

data MediaType = MTApplicationJSON | MTGeoJSON | MTTextCSV | MTTextPlain | MTTextXML | MTOpenAPI | MTUrlEncoded | MTOctetStream | MTAny | MTOther Text -- vendored media types | MTVndArrayJSONStrip | MTVndSingularJSON Bool | MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]

其中MTVndPlan携带格式(PlanJSON/PlanText)与选项(analyzeverbosesettingsbufferswal),并可通过for="..."参数指定目标媒体类型。

无法识别的媒体类型会报错

任何无法识别的媒体类型都会导致错误。例如:

curl "http://localhost:3000/people" \ -H "Accept: unknown/unknown"

返回:

HTTP/1.1 415 Unsupported Media Type {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}

该错误码在 Error.hs 中定义:code MediaTypeError{} = "PGRST107",对应 HTTP 415 状态。从negotiateContent的实现可以看到,MediaTypeError中携带的是客户端 Accept 的完整媒体类型列表(map MediaType.toMime accepts),便于排错。

如需扩展可接受的媒体类型,可以使用自定义媒体类型处理器(custom media types),详见 media_type_handlers.rst。

单数或复数响应(Singular or Plural)

默认情况下,PostgREST总是以数组形式返回 JSON 结果,即使只有一条记录。例如请求/items?id=eq.1返回:

[ { "id": 1 } ]

这对某些客户端代码可能不够方便。要返回不被数组包裹的单数对象(第一条结果),在Accept头中指定vnd.pgrst.object

curl "http://localhost:3000/items?id=eq.1" \ -H "Accept: application/vnd.pgrst.object+json"

此时返回:

{ "id": 1 }

注意:vnd.pgrst.objectvnd.pgrst.array属于供应商媒体类型,具有特殊处理逻辑,不能被自定义媒体类型处理器覆盖。这一点在 Plan/Negotiate.hs 中有明确注释:"all the vendored media types have special handling as they have media type parameters, they cannot be overridden",对应测试见 CustomMediaSpec.hs。

空结果时的行为差异

当请求单数响应但未找到任何记录时,服务端返回错误消息和 406 Not Acceptable 状态码,而不是通常的空数组 + 200:

{ "code": "PGRST116", "message": "Cannot coerce the result to a single JSON object", "details": "The result contains 0 rows", "hint": null }

源码中的判定逻辑位于 MainTx.hs:

failNotSingular :: MediaType -> ResultSet -> DbHandler () failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} = when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do lift SQL.condemn throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal

即:只要媒体类型是单数 JSON(无论是否剥离 null),且查询总行数queryTotal不等于 1,就抛出SingularityError。该错误的 HTTP 状态码为 406(见 Error.hs),错误码为PGRST116details字段动态携带实际行数("The result contains " <> show n <> " rows",见 Error.hs)。这也意味着:查询返回多于 1 行时同样会触发该错误queryTotal /= 1),单数响应要求查询结果恰好为一行。

为什么不用/stories/1这种嵌套 URL?

很多 API 用特殊的嵌套 URL 约定来区分单数和复数资源,如/stories/stories/1。PostgREST 使用/stories?id=eq.1的原因在于:

  • 单数资源(对 PostgREST 而言)是由主键确定的一行,而主键可以是复合主键(跨越多个列);
  • 常见的嵌套 URL 只考虑了简单且绝大多数为数值型主键的特例,这些所谓的"人工键"(artificial keys)通常由对象关系映射(ORM)库自动引入;
  • 诚然,PostgREST 可以检测到"对所有构成主键的列都有等值条件"从而自动转换为单数,但这可能导致格式的意外变化——客户端仅仅因为多过滤了一个列,响应格式就从对象变成数组,从而破坏已有客户端代码;
  • 因此,PostgREST 选择让手动显式指定单数/复数,把这一选择与 URL 格式解耦,行为可预测。

剥离空值(Stripped Nulls)

默认情况下,PostgREST 返回所有JSON null 值。例如请求/projects?id=gt.10返回:

[ { "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" }, { "id": 12, "name": "ProjectX", "client_id": null, "another_col": null }, { "id": 13, "name": "Y", "client_id": null, "another_col": null } ]

在大结果集上,值为null的未使用键会白白浪费带宽。要移除它们,把nulls=stripped作为application/vnd.pgrst.array的参数:

curl "http://localhost:3000/projects?id=gt.10" \ -H "Accept: application/vnd.pgrst.array+json;nulls=stripped"

此时返回:

[ { "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" }, { "id": 12, "name": "ProjectX" }, { "id": 13, "name": "Y" } ]

源码中的实现细节

在 MediaType.hs 的decodeMediaType中,nulls参数通过解析媒体类型参数获得:

("application", "vnd.pgrst.object+json", _) -> MTVndSingularJSON strippedNulls ("application", "vnd.pgrst.object", _) -> MTVndSingularJSON strippedNulls ("application", "vnd.pgrst.array+json", _) -> checkArrayNullStrip ("application", "vnd.pgrst.array", _) -> checkArrayNullStrip ... strippedNulls = fromMaybe "false" (params !? "nulls") == "stripped" checkArrayNullStrip = if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON

关键点:

  • 参数名不区分大小写(按 RFC 7231 规范归一化为小写);
  • nulls参数只有精确等于stripped时才生效
  • application/vnd.pgrst.array+json未带nulls=stripped时,等价于普通的application/jsoncheckArrayNullStrip返回MTApplicationJSON);
  • application/vnd.pgrst.object+json;nulls=stripped表示"单数响应 + 剥离 null"的组合,MTVndSingularJSON Bool中的Bool正是用于记录是否剥离 null。

对应响应时的Content-Type由 MediaType.hs 的toMime生成:

toMime MTVndArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped" toMime (MTVndSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped" toMime (MTVndSingularJSON False) = "application/vnd.pgrst.object+json"

该功能的完整行为有专门测试覆盖,见 NullsStripSpec.hs,其中分别验证了application/vnd.pgrst.array+json;nulls=strippedapplication/vnd.pgrst.array;nulls=stripped(省略+json后缀)两种写法。

补充:供应商媒体类型对 OpenAPI 的影响

在 Response/OpenAPI.hs 中可以看到,PostgREST 生成的 OpenAPI 文档把producesconsumes声明为[MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV],即文档明确告知客户端:端点可产出/消费 JSON、单数 JSON(含剥离 null 变体)与 CSV。

请求体(Request Body)

服务端处理以下请求体媒体类型:

  • application/json
  • application/x-www-form-urlencoded
  • text/csv

对于表/视图(见 tables_views.rst),上述媒体类型适用于POSTPATCHPUT方法;对于函数(见 functions.rst),适用于POST方法。

对于函数,还有三种额外的请求体媒体类型:

  • application/octet-stream
  • text/plain
  • text/xml

这些二进制/文本/XML 类型的请求体用于向函数传递单一未命名参数(single unnamed argument),详见 functions.rst 中的相关章节。

源码中的请求体解析

请求体的Content-Type头在 ApiRequest.hs 中被解析(默认application/json),实际解析逻辑位于 ApiRequest/Payload.hs:

(MTTextCSV, _) -> do json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody) note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json (MTUrlEncoded, True) -> Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params) ... (MTTextPlain, True) -> Right $ RawPay reqBody (MTTextXML, True) -> Right $ RawPay reqBody (MTOctetStream, True) -> Right $ RawPay reqBody (ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct

从中可以确认:

  • CSV 请求体会被解析为 JSON(使用CSV.decodeByName,要求所有行的字段数一致,否则报错);
  • application/x-www-form-urlencoded被解析为参数映射;
  • text/plaintext/xmlapplication/octet-stream仅对函数调用(True表示是过程调用)以原始字节形式(RawPay)传递;
  • 不支持的Content-Type返回"Content-Type not acceptable: ..."错误。

小结

PostgREST 的资源表示能力可归纳为一张速查表:

需求写法
指定响应格式Accept: application/jsonAccept: text/csvAccept: application/geo+json
单数响应Accept: application/vnd.pgrst.object+json
剥离 null 的数组Accept: application/vnd.pgrst.array+json;nulls=stripped
单数 + 剥离 nullAccept: application/vnd.pgrst.object+json;nulls=stripped
查看执行计划Accept: application/vnd.pgrst.plan+json(需启用 db-plan,见 media_type_handlers.rst)
请求体格式Content-Type: application/jsonapplication/x-www-form-urlencodedtext/csv(函数另支持text/plaintext/xmlapplication/octet-stream

需要记住的三个关键行为:

  1. 列顺序不保证:不要依赖 JSON 响应中字段的排列顺序;
  2. 单数响应是显式行为vnd.pgrst.object在结果不为恰好一行时返回 406(PGRST116),不会静默降级;
  3. 供应商媒体类型不可被自定义处理器覆盖vnd.pgrst.*系列由 PostgREST 内置逻辑独占处理。

通过合理组合Accept头与媒体类型参数,你可以在不改动数据库与业务代码的前提下,为不同客户端(浏览器、移动端、数据管道)提供最贴合其需求的资源表示。

【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 1:37:19

动态三维重构毫秒级极速响应技术实现白皮书

1 概述1.1 技术背景现代智能化作战呈现高速对抗、突发临机、快节奏博弈的典型特征&#xff0c;战场制胜权由传统兵力、火力优势逐步转向认知速度、感知时延、决策闭环效率的体系优势。OODA作战循环的极速压缩&#xff0c;已成为快速反应作战、应急处置、突发对抗场景的核心制胜…

作者头像 李华
网站建设 2026/9/11 1:36:59

分布式计算核心原理与实战:从引擎选型到集群部署避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 1:35:39

编程课程第二章作业设计:从基础到实践

1. 项目概述"第2章作业"这个标题看似简单&#xff0c;实则包含了丰富的教学内涵。作为一线教育工作者&#xff0c;我深知章节作业在知识巩固和能力培养中的关键作用。这类作业通常出现在教材或课程的第二章节之后&#xff0c;旨在检验学生对基础概念的掌握程度&#…

作者头像 李华
网站建设 2026/9/11 1:35:32

SpringBoot汽车美容平台开发与优化实践

1. 项目概述&#xff1a;汽车美容行业数字化解决方案这个基于SpringBoot的汽车美容平台项目&#xff0c;是我为本地一家连锁汽车服务企业开发的数字化管理系统。传统汽车美容行业长期面临服务流程不透明、客户管理混乱、员工绩效难量化等痛点。通过这套系统&#xff0c;我们实现…

作者头像 李华
网站建设 2026/9/11 1:27:34

Vosk语音识别不准?实战拉高识别准确率的3个调优开关

Vosk语音识别不准&#xff1f;实战拉高识别准确率的3个调优开关 【免费下载链接】vosk-api Offline speech recognition API for Android, iOS, Raspberry Pi and servers with Python, Java, C# and Node 项目地址: https://gitcode.com/GitHub_Trending/vo/vosk-api V…

作者头像 李华