将 json 转换为嵌套的 postgres 复合类型

Posted

技术标签:

【中文标题】将 json 转换为嵌套的 postgres 复合类型【英文标题】:Converting json to nested postgres composite type 【发布时间】:2016-10-14 16:24:50 【问题描述】:

我在 postgres 中定义了以下嵌套类型:

CREATE TYPE address AS (
  name    text,
  street  text,
  zip     text,
  city    text,
  country text
);

CREATE TYPE customer AS (
  customer_number           text,
  created                   timestamp WITH TIME ZONE,
  default_billing_address   address,
  default_shipping_address  address
);

现在想在一个存储过程中填充这些类型,该过程将 json 作为输入参数。这适用于***字段,输出显示了 postgres 复合类型的内部格式:

# select json_populate_record(null::customer, '"customer_number":"12345678"'::json)::customer;
 json_populate_record 
----------------------
 (12345678,,,)
(1 row)

但是,postgres 不处理嵌套的 json 结构:

# select json_populate_record(null::customer, '"customer_number":"12345678","default_shipping_address":"name":"","street":"","zip":"12345","city":"Berlin","country":"DE"'::json)::customer;
ERROR:  malformed record literal: ""name":"","street":"","zip":"12345","city":"Berlin","country":"DE""
DETAIL:  Missing left parenthesis.

再次起作用的是,如果嵌套属性是 postgres 的内部格式,如下所示:

# select json_populate_record(null::customer, '"customer_number":"12345678","default_shipping_address":"(\"\",\"\",12345,Berlin,DE)"'::json)::customer;
            json_populate_record            
--------------------------------------------
 (12345678,,,"("""","""",12345,Berlin,DE)")
(1 row)

有没有办法让 postgres 从嵌套的 json 结构转换为相应的复合类型?

【问题讨论】:

【参考方案1】:

json_populate_record() 仅用于嵌套对象:

with a_table(jdata) as (
values
    ('
        "customer_number":"12345678",
        "default_shipping_address":
            "name":"",
            "street":"",
            "zip":"12345",
            "city":"Berlin",
            "country":"DE"
        
    '::json)
)
select (
    jdata->>'customer_number', 
    jdata->>'created', 
    json_populate_record(null::address, jdata->'default_billing_address'),
    json_populate_record(null::address, jdata->'default_shipping_address')
    )::customer
from a_table;

                    row                     
--------------------------------------------
 (12345678,,,"("""","""",12345,Berlin,DE)")
(1 row) 

嵌套复合类型不是 Postgres(和任何 RDBMS)的设计目的。它们太复杂和麻烦了。 在数据库逻辑中,嵌套结构应该被维护为相关的表,例如

create table addresses (
    address_id serial primary key,
    name text,
    street text,
    zip text,
    city text,
    country text
);

create table customers (
    customer_id serial primary key, -- not necessary `serial` may be `integer` or `bigint`
    customer_number text,           -- maybe redundant
    created timestamp with time zone,
    default_billing_address int references adresses(address_id),
    default_shipping_address int references adresses(address_id)
);

有时在表格中使用嵌套结构是合理的,但在这些情况下使用jsonbhstore 似乎更方便和自然,例如:

create table customers (
    customer_id serial primary key, 
    customer_number text,
    created timestamp with time zone,
    default_billing_address jsonb,
    default_shipping_address jsonb
);

【讨论】:

这可行,但对于更大或更深的嵌套对象,我想避免在复合类型中重复所有列及其顺序。 恐怕你别无选择。 为了提供更多关于嵌套类型使用的上下文,这些将是存储过程或查询的返回值,基础表将被很好地规范化。 Zalando 在他们的 java-sproc-wrapper (github.com/zalando-incubator/java-sproc-wrapper/#type-mapping) 中在客户端做类似的事情,但这首先必须从数据库中提取所有类型信息。通过将类型映射到 json 和从 json 映射,实现可以简单得多。 那是误会。如果服务器必须响应结构化数据,它应该发送简单的 json,可以在客户端轻松解释,无需任何额外工具。我仍然看不出有任何理由使用嵌套复合类型。您无法将它们映射到服务器端的相关表中,因为这完全没用。好吧,我可以想象将嵌套复合类型用于常见的对象类型,但这需要创建特殊的工具来支持(组合、分解、分配、修改、比较)此类数据。【参考方案2】:

plpython 救援:

create function to_customer (object json)
returns customer
AS $$
import json
return json.loads(object)
$$ language plpythonu;

例子:

select to_customer('
        "customer_number":"12345678",
        "default_shipping_address":
        
                "name":"",
                "street":"",
                "zip":"12345",
                "city":"Berlin",
                "country":"DE"
        ,
        "default_billing_address":null,
        "created": null
'::json);
                to_customer                 
--------------------------------------------
 (12345678,,,"("""","""",12345,Berlin,DE)")
(1 row)

警告:从 python 构建返回的对象时,postgresql 需要将所有 null 值呈现为 None(即不允许跳过不存在的空值),因此我们必须指定传入的所有空值json。例如不允许:

select to_customer('
        "customer_number":"12345678",
        "default_shipping_address":
        
                "name":"",
                "street":"",
                "zip":"12345",
                "city":"Berlin",
                "country":"DE"
         
'::json);                             
ERROR:  key "created" not found in mapping
HINT:  To return null in a column, add the value None to the mapping with the key named after the column.
CONTEXT:  while creating return value
PL/Python function "to_customer"

【讨论】:

虽然我可能不想依赖 plpython 来解决这个问题,但这个答案表明有一个通用的解决方案,并且 postgres 将拥有实现它所需的所有类型信息。【参考方案3】:

这似乎在 Postgres 10 中得到解决。在 release notes 中搜索 json_populate_record 显示以下更改:

使 json_populate_record() 和相关函数递归处理 JSON 数组和对象 (Nikita Glukhov)

通过此更改,目标 SQL 类型中的数组类型字段从 JSON 数组正确转换,复合类型字段从 JSON 对象正确转换。以前,这种情况会失败,因为 JSON 值的文本表示将被提供给 array_in() 或 record_in(),并且其语法与这些输入函数所期望的不匹配。

【讨论】:

以上是关于将 json 转换为嵌套的 postgres 复合类型的主要内容,如果未能解决你的问题,请参考以下文章

如何将 postgres 数据库转换为 JSON 文件?

AWS Glue 将字符串值从 postgres 转换为 json 数组

将嵌套 JSON 转换为简单 JSON

将嵌套 JSON 转换为数据框

将嵌套 JSON 转换为平面 JSON

将 Pandas 数据框转换为嵌套 JSON