web-dev-qa-db-ja.com

PostgreSQLトリガーにユーザーIDを渡す

PostgreSQL 9.1を使用しています。データベースは、アプリケーションが使用する実際のテーブルが存在するように構造化されています。すべてのテーブルには、変更履歴のみを格納する履歴テーブルがあります。履歴テーブルには、実際のテーブルとフィールドが同じフィールドを含み、フィールドがいくつかの追加情報を形成します。時間を編集します。履歴テーブルはトリガーによってのみ処理されます。

私には2種類のトリガーがあります:

  1. Before INSERTトリガーは、テーブルが作成されたときに追加情報をテーブルに追加します(例:create_time)。
  2. Before UPDATEトリガーおよびbefore DELETEトリガーは、古い値を実際のテーブルから履歴テーブルにコピーします。

問題は、トリガーを使用して、それらの変更を行ったユーザーのIDも保存したいことです。 IDとは、PostgreSQLユーザーIDではなく、phpアプリケーションからのIDを意味します。

それを行う合理的な方法はありますか?

INSERTとUPDATEを使用すると、IDのフィールドを実際のテーブルに追加し、SQLクエリの一部としてSQLにユーザーIDを渡すだけで済みます。私の知る限り、これはDELETEでは機能しません。

すべてのトリガーは次のように構成されています。

CREATE OR REPLACE FUNCTION before_delete_customer() RETURNS trigger AS $BODY$
BEGIN
    INSERT INTO _customer (
        edited_by,
        edit_time,
        field1,
        field2,
        ...,
        fieldN
    ) VALUES (
        -1, // <- This should be user id.
        NOW(),
        OLD.field1,
        OLD.field2,
        ...,
        OLD.fieldN
    );
    RETURN OLD;
END; $BODY$
LANGUAGE plpgsql
38
pipo

オプションは次のとおりです。

  • 接続を開くと、CREATE TEMPORARY TABLE current_app_user(username text); INSERT INTO current_app_user(username) VALUES ('the_user');になります。次に、トリガーで_SELECT username FROM current_app_user_を使用して、おそらくサブクエリとして現在のユーザー名を取得します。

  • _postgresql.conf_で、_my_app.username = 'unknown';_のように カスタムGUC のエントリを作成します。接続を作成するたび run _SET my_app.username = 'the_user';_ 。次に、トリガーで current_setting('my_app.username') function を使用して値を取得します。事実上、セッション変数を提供するためにGUC機構を悪用しています。 カスタムGUCが9.2で変更されたため、サーバーバージョンに適したドキュメントを読んでください。

  • すべてのアプリケーションユーザーのデータベースロールを持つようにアプリケーションを調整します。作業を行う前に、そのユーザーに_SET ROLE_を送信します。これにより、_current_user_に組み込みの_SELECT current_user;_変数のような関数を使用できるだけでなく、データベースにセキュリティを強制することもできますこの質問 を参照してください。 _SET ROLE_を使用する代わりに、ユーザーとして直接ログインすることもできますが、その場合、接続プールが困難になる傾向があります。

3つすべてのケースで接続プーリングを行っている場合、プールに接続を返すときは _DISCARD ALL;_ に注意する必要があります。 ( そのように記載されていないが 、_DISCARD ALL_は_RESET ROLE_を実行します)。

デモの一般的なセットアップ:

_CREATE TABLE tg_demo(blah text);
INSERT INTO tg_demo(blah) VALUES ('spam'),('eggs');

-- Placeholder; will be replaced by demo functions
CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
SELECT 'unknown';
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION tg_demo_trigger() RETURNS trigger AS $$
BEGIN
    RAISE NOTICE 'Current user is: %',get_app_user();
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tg_demo_tg
AFTER INSERT OR UPDATE OR DELETE ON tg_demo 
FOR EACH ROW EXECUTE PROCEDURE tg_demo_trigger();
_

GUCの使用:

  • _CUSTOMIZED OPTIONS_の_postgresql.conf_セクションに、_myapp.username = 'unknown_user'_のような行を追加します。 9.2より古いPostgreSQLバージョンでは、_custom_variable_classes = 'myapp'_も設定する必要があります。
  • PostgreSQLを再起動します。これで_SHOW myapp.username_を取得して、値_unknown_user_を取得できるようになります。

これで、接続を確立するときに_SET myapp.username = 'the_user';_を使用できます。または、トランザクションをローカルにしたい場合は、トランザクションをBEGINningした後に_SET LOCAL myapp.username = 'the_user';_を使用できます。これは、プールされた接続に便利です。

_get_app_user_関数の定義:

_CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
    SELECT current_setting('myapp.username');
$$ LANGUAGE sql;
_

トランザクションローカルの現在のユーザー名に_SET LOCAL_を使用したデモ:

_regress=> BEGIN;
BEGIN
regress=> SET LOCAL myapp.username = 'test_user';
SET
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: test_user
INSERT 0 1
regress=> COMMIT;
COMMIT
regress=> SHOW myapp.username;
 myapp.username 
----------------
 unknown_user
(1 row)
_

_SET LOCAL_の代わりにSETを使用する場合、設定はコミット/ロールバック時に元に戻されないため、セッション全体で永続的です。それでも_DISCARD ALL_によってリセットされます:

_regress=> SET myapp.username = 'test';
SET
regress=> SHOW myapp.username;
 myapp.username 
----------------
 test
(1 row)

regress=> DISCARD ALL;
DISCARD ALL
regress=> SHOW myapp.username;
 myapp.username 
----------------
 unknown_user
(1 row)
_

また、SETまたは_SET LOCAL_をサーバー側のバインドパラメータと共に使用することはできません。バインドパラメータ(「準備されたステートメント」)を使用する場合は、set_config(...)という形式の関数の使用を検討してください。参照 システム管理関数

一時テーブルを使用する

このアプローチでは、セッションごとに一時テーブルから値を読み取ろうとするトリガー(または、できればトリガーによって呼び出されるヘルパー関数)を使用する必要があります。一時テーブルが見つからない場合は、デフォルト値が提供されます。これはやや遅いと思われます。慎重にテストしてください。

get_app_user()の定義:

_CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
DECLARE
    cur_user text;
BEGIN
    BEGIN
        cur_user := (SELECT username FROM current_app_user);
    EXCEPTION WHEN undefined_table THEN
        cur_user := 'unknown_user';
    END;
    RETURN cur_user;
END;
$$ LANGUAGE plpgsql VOLATILE;
_

デモ:

_regress=> CREATE TEMPORARY TABLE current_app_user(username text);
CREATE TABLE
regress=> INSERT INTO current_app_user(username) VALUES ('testuser');
INSERT 0 1
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: testuser
INSERT 0 1
regress=> DISCARD ALL;
DISCARD ALL
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: unknown_user
INSERT 0 1
_

安全なセッション変数

「安全なセッション変数」をPostgreSQLに追加する提案もあります。これらはパッケージ変数に少し似ています。 PostgreSQL 12の時点では、この機能は含まれていませんが、必要な場合は、ハッカーリストに目を配り、発言してください。

Advanced:共有メモリ領域を備えた独自の拡張機能

高度な使用法では、独自のC拡張で共有メモリ領域を登録し、DSAセグメントの値を読み書きするC関数呼び出しを使用してバックエンド間で通信することもできます。詳細については、PostgreSQLプログラミング例を参照してください。 Cの知識、時間、忍耐力が必要です。

43
Craig Ringer

セットには、ここで説明されていないバリアントセットセッションがあります。これは、アプリケーション開発者がプレーンセットやローカルセットではなく、通常実際に必要とするものです。

set session trolol.userr = 'Lol';

テストトリガーのセットアップは少し単純ですが、アイデアはCraig Ringerのオプション2と同じです。

create table lol (
    pk varchar(3) not null primary key,
    createuser varchar(20) not null);


CREATE OR REPLACE function update_created() returns trigger as $$ 
     begin new.createuser := current_setting('trolol.userr'); return new; end; $$ language plpgsql;


create trigger lol_update before update on lol for each row execute procedure update_created();
create trigger lol_insert before insert on lol for each row execute procedure update_created();

この時点で、これはまったく問題ないと思います。セッション変数が何らかの理由で誤って設定されていない場合、DDLステートメントはなく、挿入/更新は成功しません。

DISCARD ALLを使用すると、すべてが破棄されるため、良い方法とは言えません。たとえば、SqlKormaはこれがまったく好きではありません。代わりに、使用して変数をリセットできます

SET software.theuser TO DEFAULT

私が簡単に検討した4番目のオプションがありました。変数の標準セットには、使用できる「application_name」があります。このソリューションにはいくつかの制限がありますが、状況によっては明らかな利点もあります。

この4番目のオプションの詳細については、以下を参照してください。

JDBCを介してapplication_nameを設定

application_nameに関するpostgreドキュメント

10
lokori