web-dev-qa-db-ja.com

トリガーを使用して、挿入または更新時にjson列のフィールドと列を同期します

私は少しデータベース/ postgresの初心者なので、我慢してください。
テーブルがある場合、このようなもの。

CREATE TABLE testy (
    id INTEGER REFERENCES other_table,
    name varchar(128) PRIMARY KEY,
    json JSONB NOT NULL
);

idnamejsonの同じ名前のフィールドの値に設定する挿入または更新の前にトリガーを作成しようとしています。

したがって、たとえば、testyに以下が含まれ、UPDATE testy SET json = '{"id":2,"name":"jim"}' WHERE id = 1が呼び出された場合。

id | name | json
---+------+-----
 1 | "jim"| {"id":1,"name":"jim"}

望ましい結果は

id | name | json
---+------+-----
 2 | "jim"| {"id":2,"name":"jim"}

これをかなり一般的にしたいので、列名をハードコーディングする必要はありません。対応するjsonフィールドが存在しない場合は、列をNULLに設定しても問題ありません。これまでのところ

CREATE TABLE testy_index (
    id INTEGER PRIMARY KEY
);

INSERT INTO testy_index VALUES (1);
INSERT INTO testy_index VALUES (2);
INSERT INTO testy_index VALUES (3);

CREATE TABLE testy (
    id INTEGER REFERENCES testy_index,
    json JSONB NOT NULL
);

CREATE UNIQUE INDEX testy_id ON testy((json->>'id'));

CREATE OR REPLACE FUNCTION json_fn() RETURNS TRIGGER AS $testy$
    DECLARE
        roow RECORD;
    BEGIN
        FOR roow IN 
            SELECT column_name FROM information_schema.columns WHERE table_name = 'testy'
        LOOP
            NEW.roow.column_name = (NEW.json->>roow.column_name);
        END LOOP;
    END;
$testy$ LANGUAGE plpgsql;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();

Roow.column_nameを柔軟に使用できないため、これは機能しません。 EXECUTEをいじってみましたが、うまくいきませんでした。

どんな助けでも大歓迎です!!

編集:これの動機は、jsonフィールドとして動作するものに外部キー制約を設定できるようにするためです。

編集:plv8は素晴らしいです。 @DanielVéritéの回答の修正版を使用して、jsonでフィールドとして表されていない列がnullになるようにしました

CREATE OR REPLACE FUNCTION json_fn() RETURNS trigger AS
$$
  var obj = JSON.parse(NEW.json);
  for(var col in NEW){
      if(col == 'json'){
        continue;
      }
      if(col in obj){
        NEW[col]=obj[col];
      }else{
        NEW[col]=null;
      }
  }
  return NEW;
$$
LANGUAGE plv8;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();
4
kag0

実際、これで十分です:

NEW := jsonb_populate_record(NEW, NEW.json);

ドキュメントごと:

jsonb_populate_record(base anyelement, from_json jsonb)

from_jsonのオブジェクトを、baseで定義されたレコードタイプと列が一致する行に展開します(下記の注を参照)。

What'snotdocumented: 最初の引数として提供された行は、上書きされないすべての値を保持します(json値に一致するキーがありません)。これが変更される理由はわかりませんが、文書化されていない限り、完全に信頼することはできません。

注意すべきことの1つ-あなたが書いた:

対応するjsonフィールドが存在しない場合は、列をNULLに設定しても問題ありません。

これは、JSON値に一致するキーがないすべての値を保持します。これはさらに優れているはずです。

「文書化されていない」ことが不明確である場合は、hstore演算子 #= を使用してくださいまったく同じ

NEW := (NEW #= hstore(jsonb_populate_record(NEW, NEW.json)));

とにかく、ほとんどのシステムにhstoreモジュールをインストールする必要があります。手順:

両方の解決策は、私の回答から導き出すことができます Danielはすでに参照されています

機能コード

CREATE OR REPLACE FUNCTION json_fn()
  RETURNS TRIGGER AS
$func$
BEGIN
   NEW := jsonb_populate_record(NEW, NEW.json); -- or hstore alternative
   RETURN NEW;
END
$func$ LANGUAGE plpgsql;

セットアップの他のすべては正しく見えます。PKをtestyに追加するだけです。

CREATE TABLE testy (
    id   int   PRIMARY KEY REFERENCES testy_index
  , data jsonb NOT NULL
);

9.4ページでテストしましたが、宣伝どおりに動作します。 PLv8機能がパフォーマンスとシンプルさに匹敵するかどうかは疑問です。

他の列をNULLに設定する

コメント通り:

CREATE OR REPLACE FUNCTION json_fn()
  RETURNS TRIGGER AS
$func$
DECLARE
  _j jsonb := NEW.json;  -- remember the json value
BEGIN
   NEW := jsonb_populate_record(NULL::testy, _j);
   NEW.json := _j;   -- reassign
   RETURN NEW;
END
$func$ LANGUAGE plpgsql;

明らかに、列名またはjsonb列がJSON値のキー名として表示されないようにする必要があります。また、jsonはデータ型名であり、混乱を招く可能性があるため、列名としては使用しません。

2

動的フィールドは、plpgsqlでは悪名高いほど困難です。特に、new.variable := somethingを書く方法はありません。ここで、variableは列名を表します。

実行時にカタログを照会する方法については、 動的SQLを使用して複合変数フィールドの値を設定する方法 を参照してください。

個人的には、plv8言語を使用したより簡単なソリューションをお勧めします。

CREATE FUNCTION json_fn() RETURNS trigger AS
$$
  var obj = JSON.parse(NEW.json);
  for (var key in obj) {
    NEW[key]=obj[key];
  }
  return NEW;
$$
LANGUAGE plv8;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();

それ以外の場合は、jsonフィールドをこのテーブルから別のテーブルに移動しても、実装が単純化されていないかどうかを検討できます。

3
Daniel Vérité

私は少し時間をかけてこの質問の答えを作成し、あなたのニーズに合うかもしれませんが、詳細な基準がないので、完全ではないかもしれません。しかし、うまくいけば、設計のニーズに合わせて操作できるほど十分に近いです。

最初の仮定

最初に、アルゴリズムを設計するためにいくつかの初期仮定を行わなければなりませんでした。

1)この関数を使用すると、jsonの更新と「外部キー」列の更新を同時に実行しているテーブルの名前にアクセスできます。 (私がしたように)変数であるか、関数の各インスタンスに個別にハードコードする必要があります。

2)最初に指定したUPDATEクエリに基づいて、_UPDATE testy SET json = '{"id":2,"name":"jim"}' WHERE id = 1_として設計されていることを確認しました。これは、アプリケーションの他の場所に方法があることを示しています述語条件を取得するには、_WHERE id = 1_。したがって、これは関数への入力としても機能します。

これを多数のテーブルに適用できる単一の関数として使用し、その再利用性を高めることを意図している場合、関連するjson列の名前が各テーブルで同一である必要があります。それ以外の場合は、列のデータ型を確認するために追加の作業を行う必要があり、他のいくつかのケースでは、これがどこに問題があるのか​​を考えることができます。これらの列すべてに_json_field_と名前を付けるだけで大​​丈夫です。

さらに面倒なく、関数に進みます。

関数定義

_CREATE OR REPLACE FUNCTION json_prop(json_entry json, table_update text, where_clause text)  
RETURNS void AS                                 
$func$
DECLARE
   sql text := 'UPDATE ';
   colname_row RECORD;
BEGIN

  sql := sql || table_update || ' SET ';

  FOR colname_row IN 
  (SELECT col_tab.column_name FROM 
  (SELECT column_name FROM information_schema.columns WHERE table_name = table_update)
  AS col_tab 
  WHERE (json_entry->column_name) IS NOT NULL) 
  LOOP
    sql := sql || colname_row.column_name::text || ' = ''' || ((json_entry)->>(colname_row.column_name::text)) || ''', ';
  END LOOP;

  sql := sql || 'json_field = ''' || json_entry || ''' ';
  sql := sql || where_clause || ';';

   EXECUTE sql;
END
$func$ LANGUAGE plpgsql;
_

つまり、簡単に言うと、関数は3つの入力引数を取り、それらを使用して動的SQLステートメントを生成し、それを実行します。

入力

_json_entry_-まさにあなたがそう思うもの。更新するjsonエントリ。

_table_update_-更新するターゲットテーブル。

_where_clause_-先に述べたように、私はあなたが事前に確立した述語を持っているというあなたの説明に基づいて仮定をしたので、そのエントリーはここに行きます。

操作

この関数は、副選択_table_update_を実行することにより、_json_entry_フィールドのキーと名前が一致する列を検索して、テーブル_SELECT column_name FROM information_schema.columns WHERE table_name = table_update_の列を検索します。

列名と一致するjsonキーの場合、その列名は外側のselect SELECT col_tab.column_name FROM ... AS col_tab WHERE (json_entry->column_name) IS NOT NULL)によって返されます。

FORループはこれらの一致する各列名を反復処理し、関連する列データを更新するために必要な情報を動的SQLステートメントに追加します。

[〜#〜]注[〜#〜]「無関係な」フィールドは無視されました。つまり、一致する列名がないjsonキーがある場合、または_json_entry_入力にない列名がある場合、これらのフィールドは無視されます。

関数の呼び出し

関数を呼び出すだけで呼び出すことができます

_SELECT * FROM json_prop('{"id":2,"name":"james"}'::json, 'testy', 'WHERE id = 1');
_

可能な変更

繰り返しますが、これはあなたのニーズに完全ではないかもしれません。トリガーとして使用することを検討しているとのことですが、代わりにトリガーをRETURNしてトリガーを設定する必要があります。 「余分な」フィールドを無視したのが気に入らない場合があり、列をNULLにしたり、これらの場合にエラーを報告したりできますか?多分私は述語へのアクセスについて間違った仮定をしたのでしょうか?

これは確かに完全な機能実装ではありませんが、ニーズに合わせて修正するだけで十分でしょう。

がんばって!

2
Chris