域表示

域表示将“数据呈现方式”与“数据存储方式”分离。它通过创建 转换 来实现,后者作用于前者以不同格式呈现和接收数据。

自定义域

假设您想使用 uuid 类型作为主键,并希望将其缩短以供 Web 用户查看。

为此,让我们创建一个基于 uuid 的域。

create domain app_uuid as uuid;

-- and use it as our table PK.
create table profiles(
  id   app_uuid
, name text
);

-- some data for the example
insert into profiles values ('846c4ffd-92ce-4de7-8d11-8e29929f4ec4', 'John Doe');

域响应格式

我们可以使用 base64 编码来缩短 uuid。在本例中,让我们使用 JSON 作为我们的响应格式。

要更改 JSON 的域格式,请创建一个函数将 app_uuid 转换为 json

-- the name of the function is arbitrary
CREATE OR REPLACE FUNCTION json(app_uuid) RETURNS json AS $$
  select to_json(encode(uuid_send($1),'base64'));
$$ LANGUAGE SQL IMMUTABLE;

-- check it works
select json('846c4ffd-92ce-4de7-8d11-8e29929f4ec4'::app_uuid);
            json
----------------------------
 "hGxP/ZLOTeeNEY4pkp9OxA=="

然后创建一个 CAST,告诉 PostgREST 在每次请求 JSON 响应时自动转换它。

CREATE CAST (app_uuid AS json) WITH FUNCTION json(app_uuid) AS IMPLICIT;

这样您就可以以缩短的格式获取数据。

curl "http://localhost:3000/profiles" \
  -H "Accept: application/json"
[{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}]

注意

  • 域上的 CAST 会被 PostgreSQL 忽略,其解释留给应用程序。我们正在讨论在 pgsql-hackers 上包含域表示行为的可能性。

  • 使用 base58 编码更有意义,因为它对 URL 友好,但为了简单起见,我们使用 base64(PostgreSQL 本地支持)。

重要

在域上创建 CAST 后,您必须刷新 PostgREST 架构缓存。请参阅 架构缓存重新加载

域过滤器格式

为了使 水平过滤 与缩短的格式一起使用,您需要不同的转换。

PostgREST 将 URL 查询字符串视为最通用的 text。因此,让我们创建一个函数将 text 转换为 app_uuid

-- the name of the function is arbitrary
CREATE OR REPLACE FUNCTION app_uuid(text) RETURNS app_uuid AS $$
  select substring(decode($1,'base64')::text from 3)::uuid;
$$ LANGUAGE SQL IMMUTABLE;

-- plus a CAST to tell PostgREST to use this function
CREATE CAST (text AS app_uuid) WITH FUNCTION app_uuid(text) AS IMPLICIT;

现在您可以像往常一样过滤。

curl "http://localhost:3000/profiles?id=eq.ZLOTeeNEY4pkp9OxA==" \
  -H "Accept: application/json"
[{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}]

注意

如果没有从 textapp_uuid 的 CAST 定义,过滤器将仍然使用本机 uuid 格式 (846c4ffd-92ce-4de7-8d11-8e29929f4ec4) 工作。

域请求主体格式

要接受 JSON 请求主体中的缩短格式(例如,在创建新记录时),请定义一个 jsonapp_uuid 的转换。

-- the name of the function is arbitrary
CREATE OR REPLACE FUNCTION app_uuid(json) RETURNS public.app_uuid AS $$
  -- here we reuse the previous app_uuid(text) function
  select app_uuid($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;

CREATE CAST (json AS public.app_uuid) WITH FUNCTION app_uuid(json) AS IMPLICIT;

现在我们可以像往常一样 插入(或 更新)。

curl "http://localhost:3000/profiles" \
  -H "Prefer: return=representation" \
  -H "Content-Type: application/json" \
  -d @- <<JSON

{"id":"zH7HbFJUTfy/GZpwuirpuQ==","name":"Jane Doe"}

JSON

响应

[{"id":"zH7HbFJUTfy/GZpwuirpuQ==","name":"Jane Doe"}]

请注意,在数据库端,我们使用的是常规的 uuid 格式。

select * from profiles;

                  id                  |   name
--------------------------------------+----------
 846c4ffd-92ce-4de7-8d11-8e29929f4ec4 | John Doe
 cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9 | Jane Doe
(2 rows)

注意

如果没有定义从 jsonapp_uuid 的 CAST,请求体仍然可以使用原生 uuid 格式(cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9)。

相对于视图的优势

视图 也允许我们更改底层类型的格式。但是它们也有一些缺点,会增加复杂性。

  1. 在视图中格式化列会使其 不可更新,因为 Postgres 不知道如何反转转换。可以使用 INSTEAD OF 触发器来解决这个问题。

  2. 当按此列进行过滤时,由于相同的原因,我们会得到全表扫描(也适用于 计算字段)。可以使用计算索引或使用物化生成列来避免这里的性能损失。

  3. 如果格式化后的列用作外键,PostgREST 将无法再检测到该关系,并且 资源嵌入 会中断。可以使用 计算关系 来解决这个问题。

域表示避免了以上所有缺点。它们唯一的缺点是,对于现有表,您必须更改列类型。但这应该是一个快速的操作,因为域与它们的底层类型是二进制可强制转换的。不需要重写表。

注意

为什么不创建一个 基本类型 呢? CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)

创建基本类型需要超级用户权限,而云托管数据库中限制了超级用户权限。此外,这种方式让“数据呈现方式”决定“数据存储方式”,这将是倒退的。