ネストされた結合を使用したPostgreSQL9.2 row_to_json()


85

row_to_json()PostgreSQL 9.2で追加された関数を使用して、クエリの結果をJSONにマッピングしようとしています。

結合された行をネストされたオブジェクト(1:1の関係)として表すための最良の方法を見つけるのに苦労しています

これが私が試したものです(セットアップコード:テーブル、サンプルデータ、続いてクエリ):

-- some test tables to start out with:
create table role_duties (
    id serial primary key,
    name varchar
);

create table user_roles (
    id serial primary key,
    name varchar,
    description varchar,
    duty_id int, foreign key (duty_id) references role_duties(id)
);

create table users (
    id serial primary key,
    name varchar,
    email varchar,
    user_role_id int, foreign key (user_role_id) references user_roles(id)
);

DO $$
DECLARE duty_id int;
DECLARE role_id int;
begin
insert into role_duties (name) values ('Script Execution') returning id into duty_id;
insert into user_roles (name, description, duty_id) values ('admin', 'Administrative duties in the system', duty_id) returning id into role_id;
insert into users (name, email, user_role_id) values ('Dan', 'someemail@gmail.com', role_id);
END$$;

クエリ自体:

select row_to_json(row)
from (
    select u.*, ROW(ur.*::user_roles, ROW(d.*::role_duties)) as user_role 
    from users u
    inner join user_roles ur on ur.id = u.user_role_id
    inner join role_duties d on d.id = ur.duty_id
) row;

を使用ROW()すると、結果のフィールドを子オブジェクトに分離できることがわかりましたが、単一のレベルに制限されているようです。AS XXXこの場合に必要だと思うので、これ以上ステートメントを挿入することはできません。

::user_rolesそのテーブルの結果の場合、たとえば、を使用して適切なレコードタイプにキャストするため、列名が与えられます。

そのクエリが返すものは次のとおりです。

{
   "id":1,
   "name":"Dan",
   "email":"someemail@gmail.com",
   "user_role_id":1,
   "user_role":{
      "f1":{
         "id":1,
         "name":"admin",
         "description":"Administrative duties in the system",
         "duty_id":1
      },
      "f2":{
         "f1":{
            "id":1,
            "name":"Script Execution"
         }
      }
   }
}

私がやりたいのは、結合を追加できる方法で結合のJSONを生成し(ここでも1:1で問題ありません)、結合する親の子オブジェクトとして表現することです。つまり、次のようになります。

{
   "id":1,
   "name":"Dan",
   "email":"someemail@gmail.com",
   "user_role_id":1,
   "user_role":{
         "id":1,
         "name":"admin",
         "description":"Administrative duties in the system",
         "duty_id":1
         "duty":{
            "id":1,
            "name":"Script Execution"
         }
      }
   }
}

どんな助けでも大歓迎です。読んでくれてありがとう。


1
セットアップコードにあります。インサート。誰もが私の状況を再現できるように、すべてを設定するのに苦労しました。
dwerner 2012年

回答:


161

アップデート:PostgreSQLの9.4これは多くのことを改善の導入によりto_jsonjson_build_objectjson_objectそしてjson_build_array、それは冗長明示的にすべてのフィールドに名前を付ける必要性によるものだが、:

select
        json_build_object(
                'id', u.id,
                'name', u.name,
                'email', u.email,
                'user_role_id', u.user_role_id,
                'user_role', json_build_object(
                        'id', ur.id,
                        'name', ur.name,
                        'description', ur.description,
                        'duty_id', ur.duty_id,
                        'duty', json_build_object(
                                'id', d.id,
                                'name', d.name
                        )
                )
    )
from users u
inner join user_roles ur on ur.id = u.user_role_id
inner join role_duties d on d.id = ur.duty_id;

古いバージョンについては、読み進めてください。


単一の行に限定されるのではなく、少し面倒です。を使用して複合行タイプにエイリアスを設定することはできないASため、次の効果を実現するには、エイリアス化されたサブクエリ式またはCTEを使用する必要があります。

select row_to_json(row)
from (
    select u.*, urd AS user_role
    from users u
    inner join (
        select ur.*, d
        from user_roles ur
        inner join role_duties d on d.id = ur.duty_id
    ) urd(id,name,description,duty_id,duty) on urd.id = u.user_role_id
) row;

http://jsonprettyprint.com/を介して生成します:

{
  "id": 1,
  "name": "Dan",
  "email": "someemail@gmail.com",
  "user_role_id": 1,
  "user_role": {
    "id": 1,
    "name": "admin",
    "description": "Administrative duties in the system",
    "duty_id": 1,
    "duty": {
      "id": 1,
      "name": "Script Execution"
    }
  }
}

array_to_json(array_agg(...))ところで、1:manyの関係があるときに使用することをお勧めします。

上記のクエリは、理想的には次のように記述できる必要があります。

select row_to_json(
    ROW(u.*, ROW(ur.*, d AS duty) AS user_role)
)
from users u
inner join user_roles ur on ur.id = u.user_role_id
inner join role_duties d on d.id = ur.duty_id;

...しかし、PostgreSQLのROWコンストラクターはAS列エイリアスを受け入れません。悲しいことに。

ありがたいことに、彼らは同じことを最適化します。計画を比較します。

CTEは最適化フェンスであるため、ネストされたサブクエリバージョンを言い換えて、連鎖CTE(WITH式)を使用すると、パフォーマンスが低下する可能性があり、同じプランにはなりません。この場合row_to_jsonROWコンストラクターの列名をより直接的にオーバーライドする方法が改善されるまで、ネストされた醜いサブクエリに悩まされます。


とにかく、一般的に、原則は、列を持つjsonオブジェクトを作成しa, b, c、不正な構文を記述したい場合です。

ROW(a, b, c) AS outername(name1, name2, name3)

代わりに、行タイプの値を返すスカラーサブクエリを使用できます。

(SELECT x FROM (SELECT a AS name1, b AS name2, c AS name3) x) AS outername

または:

(SELECT x FROM (SELECT a, b, c) AS x(name1, name2, name3)) AS outername

さらに、json追加の引用符なしで値を作成できることに注意してください。たとえば、の出力をのjson_agg中に入れるrow_to_jsonと、内部のjson_agg結果は文字列として引用符で囲まれず、jsonとして直接組み込まれます。

たとえば、任意の例では:

SELECT row_to_json(
        (SELECT x FROM (SELECT
                1 AS k1,
                2 AS k2,
                (SELECT json_agg( (SELECT x FROM (SELECT 1 AS a, 2 AS b) x) )
                 FROM generate_series(1,2) ) AS k3
        ) x),
        true
);

出力は次のとおりです。

{"k1":1,
 "k2":2,
 "k3":[{"a":1,"b":2}, 
 {"a":1,"b":2}]}

json_agg積は[{"a":1,"b":2}, {"a":1,"b":2}]textそうであるように、再びエスケープされていないことに注意してください。

この手段の操作を行うことができ作曲あなたは常に呼び出して非常に複雑PostgreSQLの複合型を作成する必要はありません、行を構築するために、JSONの操作をrow_to_json出力に。


2
私があなたの答えをさらに数回賛成することができれば、私はそうします。私は詳細と1:多くの関係について少し感謝します。
dwerner 2012年

7
@dwernerお役に立ててうれしいです。良い質問を書く努力をしてくれてありがとう。私はバンプしたい、それがあまりにも数回以上アップ。サンプルデータ、Pgバージョン、期待される出力、実際の出力/エラー。すべてのボックスにチェックマークが付いており、明確で理解しやすいです。ほんとありがと。
クレイグリンガー

1
@muistooshort:タイプを提供する一時テーブルも提供され、セッションの終了時に自動的に削除されます。
Erwin Brandstetter 2014年

1
9.4の例をありがとうございました。json_build_object私の人生はずっと楽になるでしょうが、リリースノートを見たとき、どういうわけか私はそれを理解しませんでした。場合によっては、開始するために具体的な例が必要になることがあります。
ジェフ

1
スーパーアンサー-ドキュメントがjson_build_objectもう少し強調する必要があることに同意します-それは本当のゲームチェンジャーです。
bobmarksie 2017

1

長期的な保守性についての私の提案は、VIEWを使用してクエリの粗いバージョンを作成してから、次のような関数を使用することです。

CREATE OR REPLACE FUNCTION fnc_query_prominence_users( )
RETURNS json AS $$
DECLARE
    d_result            json;
BEGIN
    SELECT      ARRAY_TO_JSON(
                    ARRAY_AGG(
                        ROW_TO_JSON(
                            CAST(ROW(users.*) AS prominence.users)
                        )
                    )
                )
        INTO    d_result
        FROM    prominence.users;
    RETURN d_result;
END; $$
LANGUAGE plpgsql
SECURITY INVOKER;

この場合、オブジェクトprominence.usersはビューです。users。*を選択したので、ビューを更新してユーザーレコードにさらにフィールドを含める必要がある場合は、この関数を更新する必要はありません。


1

受け入れられた応答はN:N関係を考慮していないため、このソリューションを追加しています。別名:オブジェクトのコレクションのコレクション

あなたがN:Nの関係を持っているなら、withそれはあなたの友達です。私の例では、次の階層のツリービューを作成したいと思います。

A Requirement - Has - TestSuites
A Test Suite - Contains - TestCases.

次のクエリは結合を表しています。

SELECT reqId ,r.description as reqDesc ,array_agg(s.id)
            s.id as suiteId , s."Name"  as suiteName,
            tc.id as tcId , tc."Title"  as testCaseTitle

from "Requirement" r 
inner join "Has"  h on r.id = h.requirementid 
inner join "TestSuite" s on s.id  = h.testsuiteid
inner join "Contains" c on c.testsuiteid  = s.id 
inner join "TestCase"  tc on tc.id = c.testcaseid
  GROUP BY r.id, s.id;

複数の集計はできないため、「WITH」を使用する必要があります。

with testcases as (
select  c.testsuiteid,ts."Name" , tc.id, tc."Title"  from "TestSuite" ts
inner join "Contains" c on c.testsuiteid  = ts.id 
inner join "TestCase"  tc on tc.id = c.testcaseid

),                
requirements as (
    select r.id as reqId ,r.description as reqDesc , s.id as suiteId
    from "Requirement" r 
    inner join "Has"  h on r.id = h.requirementid 
    inner join "TestSuite" s on s.id  = h.testsuiteid

    ) 
, suitesJson as (
 select  testcases.testsuiteid,  
       json_agg(
                json_build_object('tc_id', testcases.id,'tc_title', testcases."Title" )
            ) as suiteJson
    from testcases 
    group by testcases.testsuiteid,testcases."Name"
 ),
allSuites as (
    select has.requirementid,
           json_agg(
                json_build_object('ts_id', suitesJson.testsuiteid,'name',s."Name"  , 'test_cases', suitesJson.suiteJson )
            ) as suites
            from suitesJson inner join "TestSuite" s on s.id  = suitesJson.testsuiteid
            inner join "Has" has on has.testsuiteid  = s.id
            group by has.requirementid
),
allRequirements as (
    select json_agg(
            json_build_object('req_id', r.id ,'req_description',r.description , 'test_suites', allSuites.suites )
            ) as suites
            from allSuites inner join "Requirement" r on r.id  = allSuites.requirementid

)
 select * from allRequirements

それが行うことは、アイテムの小さなコレクションでJSONオブジェクトを構築し、それらを各with節に集約することです。

結果:

[
  {
    "req_id": 1,
    "req_description": "<character varying>",
    "test_suites": [
      {
        "ts_id": 1,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 1,
            "tc_title": "TestCase"
          },
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      },
      {
        "ts_id": 2,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      }
    ]
  },
  {
    "req_id": 2,
    "req_description": "<character varying> 2 ",
    "test_suites": [
      {
        "ts_id": 2,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      }
    ]
  }
]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.