Someninesからのメモ:このブログは、Berend Toberが2018年7月16日に亡くなったため、死後に公開されています。PostgreSQLコミュニティへの彼の貢献を称え、友人とゲストライターの平和を願っています。
前回の記事では、PostgreSQLシリアル疑似タイプについて説明しました。これは、整数をインクリメントする合成キー値を入力するのに役立ちます。テーブルデータ定義言語(DDL)ステートメントでシリアルデータ型キーワードを使用することは、データベースの挿入時に単純な関数呼び出しから派生したデフォルト値が入力される整数型の列宣言として実装されることを確認しました。データ操作言語(DML)アクティビティへの統合応答の一部として機能コードを呼び出すこの自動化された動作は、PostgreSQLなどの高度なリレーショナルデータベース管理システム(RDBMS)の強力な機能です。この記事では、カスタムコードを自動的に呼び出すための別のより機能的な側面、つまりトリガーと保存された関数の使用についてさらに詳しく説明します。はじめに
トリガーと保存された関数のユースケース
トリガーと保存された関数の理解に投資する理由について話しましょう。 DMLコードをデータベース自体に組み込むことにより、データベースとのインターフェースとして構築される可能性のある複数の個別のアプリケーションでのデータ関連コードの重複実装を回避できます。これにより、データ検証、データクレンジング、またはデータ監査(変更のログ記録など)や呼び出し元のアプリケーションとは独立したサマリーテーブルの維持などの他の機能のためのDMLコードの一貫した実行が保証されます。トリガーと保存された関数のもう1つの一般的な使用法は、ビューを書き込み可能にすることです。つまり、複雑なビューの挿入や更新を有効にしたり、特定の列データを不正な変更から保護したりします。さらに、アプリケーションコードではなくサーバーで処理されるデータはネットワークを通過しないため、データが盗聴にさらされるリスクが少なくなり、ネットワークの輻輳が軽減されます。また、PostgreSQLでは、保存された関数を、セッションユーザーよりも高い特権レベルでコードを実行するように構成できます。これにより、いくつかの強力な機能が許可されます。後でいくつかの例を示します。
トリガーと保存された関数に対するケース
PostgreSQL Generalメーリングリストのコメントを確認したところ、トリガーと保存された関数の使用に不利な意見がいくつか明らかになりました。これは、完全を期すため、および実装の長所と短所を比較検討するようにチームに促すためです。
反対意見の中には、たとえば、保存された機能を維持するのは簡単ではないという認識があり、データベース管理の高度なスキルと知識を持った経験豊富な人がそれらを管理する必要がありました。一部のソフトウェア専門家は、データベースシステムの企業変更管理は通常、アプリケーションコードよりも強力であるため、ビジネスルールやその他のロジックがデータベース内に実装されている場合、要件の進展に応じて変更を加えるのは非常に面倒であると報告しています。別の観点では、トリガーを他のアクションの予期しない副作用と見なしているため、不明瞭で、見逃しやすく、デバッグが難しく、維持するのが面倒である可能性があるため、通常、最初の選択肢ではなく、最後の選択肢にする必要があります。
>これらの異議にはある程度のメリットがあるかもしれませんが、それについて考えると、データは貴重な資産であるため、実際には、企業または政府組織のRDBMSを担当する熟練した経験豊富な人またはチームが必要です。同様に、変更制御盤は、記録情報システムの持続可能な保守の実証済みのコンポーネントであり、ある人の副作用は、この記事のバランスに採用されている観点である、別の人の強力な利便性と同様です。
トリガーの宣言
ナットとボルトを学びましょう。トリガーを宣言するための一般的なDDL構文で使用できるオプションは多数あり、考えられるすべての順列を処理するにはかなりの時間がかかるため、簡潔にするために、以下の例では、それらの最小限に必要なサブセットについてのみ説明します。この簡略化された構文を使用してください:
CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
ON table_name
FOR EACH ROW EXECUTE PROCEDURE function_name()
where event can be one of:
INSERT
UPDATE [ OF column_name [, ... ] ]
DELETE
TRUNCATE
name以外に必要な構成可能な要素 いつ 、理由 、場所 、および何 つまり、トリガーアクションに関連して呼び出されるトリガーコードのタイミング(when)、トリガーDMLステートメントの特定のタイプ(why)、実行される1つまたは複数のテーブル(where)、および実行するストアドファンクションコード(何)。
関数の宣言
上記のトリガー宣言では関数名を指定する必要があるため、技術的には、トリガー関数が事前に定義されるまで、トリガー宣言DDLを実行することはできません。関数宣言の一般的なDDL構文にも多くのオプションがあるため、管理しやすくするために、ここでの目的にはこの最小限の構文を使用します。
CREATE [ OR REPLACE ] FUNCTION
name () RETURNS TRIGGER
{ LANGUAGE lang_name
| SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
}...
トリガー関数はパラメーターを受け取らず、戻りタイプはTRIGGERである必要があります。以下の例で、オプションの修飾子について説明します。
トリガーと関数の命名スキーム
尊敬されているコンピューター科学者のPhilKarltonは、名前を付けることがソフトウェアチームにとって最大の課題の1つであると(ここでは言い換えて)宣言しているとされています。ここでは、使いやすいトリガーとストアド関数の命名規則を紹介します。これは私に役立ち、独自のRDBMSプロジェクトに採用することを検討することをお勧めします。この記事の例の命名スキームは、宣言されたトリガーを示す省略形が接尾辞として付いた関連するテーブル名を使用するパターンに従います および理由 属性:最初の接尾辞は「b」、「a」、または「i」(「前」、「後」、または「代わり」のいずれか)になり、次は1つ以上の「i」になります。 、「u」、「d」、または「t」(「挿入」、「更新」、「削除」、または「切り捨て」の場合)。最後の文字はトリガーの単なる「t」です。 (ルールにも同様の命名規則を使用します。その場合、最後の文字は「r」です)。したがって、たとえば、「my_table」という名前のテーブルのさまざまな最小トリガー宣言属性の組み合わせは次のようになります。
|-------------+-------------+-----------+---------------+-----------------|
| TABLE NAME | WHEN | WHY | TRIGGER NAME | FUNCTION NAME |
|-------------+-------------+-----------+---------------+-----------------|
| my_table | BEFORE | INSERT | my_table_bit | my_table_bit |
| my_table | BEFORE | UPDATE | my_table_but | my_table_but |
| my_table | BEFORE | DELETE | my_table_bdt | my_table_bdt |
| my_table | BEFORE | TRUNCATE | my_table_btt | my_table_btt |
| my_table | AFTER | INSERT | my_table_ait | my_table_ait |
| my_table | AFTER | UPDATE | my_table_aut | my_table_aut |
| my_table | AFTER | DELETE | my_table_adt | my_table_adt |
| my_table | AFTER | TRUNCATE | my_table_att | my_table_att |
| my_table | INSTEAD OF | INSERT | my_table_iit | my_table_iit |
| my_table | INSTEAD OF | UPDATE | my_table_iut | my_table_iut |
| my_table | INSTEAD OF | DELETE | my_table_idt | my_table_idt |
| my_table | INSTEAD OF | TRUNCATE | my_table_itt | my_table_itt |
|-------------+-------------+-----------+---------------+-----------------|
トリガーと関連するストアド関数の両方にまったく同じ名前を使用できます。これは、RDBMSがトリガーとストアド関数をそれぞれの目的ごとに個別に追跡し、アイテム名が使用されるコンテキストによって作成されるため、PostgreSQLでは完全に許可されます。名前が参照しているアイテムを明確にします。
したがって、たとえば、上記の表の最初の行のシナリオに対応するトリガー宣言は、
として実装されているように見えます。CREATE TRIGGER my_table_bit
BEFORE INSERT
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_bit();
トリガーが複数の理由で宣言されている場合 属性の場合は、サフィックスを適切に展開します。たとえば、挿入または更新 トリガーすると、上記は
になりますCREATE TRIGGER my_table_biut
BEFORE INSERT OR UPDATE
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_biut();
すでにコードを見せてください!
それを現実にしましょう。簡単な例から始めて、さらに機能を説明するためにそれを拡張します。トリガーDDLステートメントには、前述のように既存の関数と、それに基づいて動作するテーブルが必要です。そのため、最初に作業するテーブルが必要です。たとえば、基本的なアカウントIDデータを保存する必要があるとします
CREATE TABLE person (
login_name varchar(9) not null primary key,
display_name text
);
一部のデータ整合性の強制は、適切な列DDLを使用して簡単に処理できます。たとえば、この場合、login_nameが存在し、9文字以下である必要があります。 login_nameのNULL値または長すぎる値を挿入しようとすると失敗し、意味のあるエラーメッセージが報告されます:
INSERT INTO person VALUES (NULL, 'Felonious Erroneous');
ERROR: null value in column "login_name" violates not-null constraint
DETAIL: Failing row contains (null, Felonious Erroneous).
INSERT INTO person VALUES ('atoolongusername', 'Felonious Erroneous');
ERROR: value too long for type character varying(9)
最小の長さを要求したり、特定の文字を拒否したりするなど、その他の強制はチェック制約で処理できます。
ALTER TABLE person
ADD CONSTRAINT PERSON_LOGIN_NAME_NON_NULL
CHECK (LENGTH(login_name) > 0);
ALTER TABLE person
ADD CONSTRAINT person_login_name_no_space
CHECK (POSITION(' ' IN login_name) = 0);
INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR: new row for relation "person" violates check constraint "person_login_name_non_null"
DETAIL: Failing row contains (, Felonious Erroneous).
INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR: new row for relation "person" violates check constraint "person_login_name_no_space"
DETAIL: Failing row contains (space man, Major Tom).
ただし、エラーメッセージは以前ほど完全に情報を提供するものではなく、意味のある説明テキストメッセージではなく、トリガー名にエンコードされている量だけを伝達することに注意してください。代わりにストアド関数にチェックロジックを実装することで、例外を使用してより役立つテキストメッセージを出力できます。また、チェック制約式にサブクエリを含めたり、現在の行の列や他のデータベーステーブル以外の変数を参照したりすることはできません。
それでは、チェック制約を削除しましょう
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;
トリガーと保存された関数を使用します。
もう少しコードを見せてください
テーブルがあります。関数DDLに移り、空のボディの関数を定義します。この関数は、後で特定のコードで入力できます。
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
SET search_path = public
AS '
BEGIN
END;
';
これにより、最終的にテーブルと関数を接続するトリガーDDLに到達できるため、いくつかの例を実行できます。
CREATE TRIGGER person_bit
BEFORE INSERT ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
PostgreSQLでは、保存された関数をさまざまな言語で作成できます。この場合と次の例では、PostgreSQL専用に設計され、PostgreSQL RDBMSのすべてのデータ型、演算子、および関数の使用をサポートするPL/pgSQL言語で関数を作成しています。 SET SCHEMAオプションは、関数の実行中に使用されるスキーマ検索パスを設定します。すべての関数の検索パスを設定すると、データベースオブジェクトにスキーマ名のプレフィックスを付ける必要がなくなり、検索パスに関連する特定の脆弱性から保護されるため、良い方法です。
例0-データ検証
最初の例として、以前のチェックを実装しましょう。ただし、より人間に優しいメッセージングを使用します。
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
AS $$
BEGIN
IF LENGTH(NEW.login_name) = 0 THEN
RAISE EXCEPTION 'Login name must not be empty.';
END IF;
IF POSITION(' ' IN NEW.login_name) > 0 THEN
RAISE EXCEPTION 'Login name must not include white space.';
END IF;
RETURN NEW;
END;
$$;
「NEW」修飾子は、挿入されようとしているデータの行への参照です。これは、トリガー関数内で使用できるいくつかの特別な変数の1つです。以下に他のいくつかを紹介します。また、PostgreSQLでは、関数本体自体に一重引用符が含まれているため、関数本体を区切る一重引用符を他の区切り文字で置き換えることができます。この場合、区切り文字として二重ドル記号を使用するという一般的な規則に従います。トリガー関数は、挿入するNEW行を返すか、アクションをサイレントに中止するためにNULLを返すことによって終了する必要があります。
同じ挿入の試みは期待どおりに失敗しますが、今ではフレンドリーなメッセージングが使用されています:
INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR: Login name must not be empty.
INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR: Login name must not include white space.
例1-監査ログ
格納された関数を使用すると、他のテーブルの参照(チェック制約では不可能)を含め、呼び出されたコードが何をするかについて幅広い自由度があります。より複雑な例として、監査テーブルの実装、つまり、プリンシパルテーブルへの挿入、更新、および削除のレコードを別のテーブルに保持する方法について説明します。監査テーブルには通常、変更された値を記録するために使用されるプリンシパルテーブルと同じ属性に加えて、変更を行うために実行された操作を記録するための追加の属性、トランザクションタイムスタンプ、およびユーザーが変更:
CREATE TABLE person_audit (
login_name varchar(9) not null,
display_name text,
operation varchar,
effective_at timestamp not null default now(),
userid name not null default session_user
);
この場合、監査の実装は非常に簡単です。既存のトリガー関数を変更してDMLを含め、監査テーブルの挿入を実行してから、トリガーを再定義して、挿入だけでなく更新時にも起動します。トリガー関数名のサフィックスを「biut」に変更しないことを選択したことに注意してください。ただし、監査機能が初期設計時に既知の要件であった場合は、それが使用される名前になります。
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
AS $$
BEGIN
IF LENGTH(NEW.login_name) = 0 THEN
RAISE EXCEPTION 'Login name must not be empty.';
END IF;
IF POSITION(' ' IN NEW.login_name) > 0 THEN
RAISE EXCEPTION 'Login name must not include white space.';
END IF;
-- New code to record audits
INSERT INTO person_audit (login_name, display_name, operation)
VALUES (NEW.login_name, NEW.display_name, TG_OP);
RETURN NEW;
END;
$$;
DROP TRIGGER person_bit ON person;
CREATE TRIGGER person_biut
BEFORE INSERT OR UPDATE ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
トリガーを起動したDML操作をそれぞれ「TRUNCATE」の「INSERT」、「UPDATE」、「DELETE」として識別するためにシステムが設定する別の特別な変数「TG_OP」を導入したことに注意してください。
属性検証テストは不要であり、削除前への入力時にNEW特殊値が定義されていないため、削除を挿入および更新とは別に処理する必要があります。 トリガー関数など、対応するストアド関数とトリガーを定義します。
CREATE OR REPLACE FUNCTION person_bdt()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
AS $$
BEGIN
-- Record deletion in audit table
INSERT INTO person_audit (login_name, display_name, operation)
VALUES (OLD.login_name, OLD.display_name, TG_OP);
RETURN OLD;
END;
$$;
CREATE TRIGGER person_bdt
BEFORE DELETE ON person
FOR EACH ROW EXECUTE PROCEDURE person_bdt();
削除されようとしている行、つまり前に存在する行への参照としてOLD特殊値を使用していることに注意してください。 削除が発生します。
機能をテストし、監査テーブルに挿入の記録が含まれていることを確認するために、いくつかの挿入を行います。
INSERT INTO person VALUES ('dfunny', 'Doug Funny');
INSERT INTO person VALUES ('pmayo', 'Patti Mayonnaise');
SELECT * FROM person;
login_name | display_name
------------+------------------
dfunny | Doug Funny
pmayo | Patti Mayonnaise
(2 rows)
SELECT * FROM person_audit;
login_name | display_name | operation | effective_at | userid
------------+------------------+-----------+----------------------------+----------
dfunny | Doug Funny | INSERT | 2018-05-26 18:48:07.6903 | postgres
pmayo | Patti Mayonnaise | INSERT | 2018-05-26 18:48:07.698623 | postgres
(2 rows)
次に、1つの行を更新し、監査テーブルに変更のレコードが含まれていることを確認し、データレコードの表示名の1つにミドルネームを追加します。
UPDATE person SET display_name = 'Doug Yancey Funny' WHERE login_name = 'dfunny';
SELECT * FROM person;
login_name | display_name
------------+-------------------
pmayo | Patti Mayonnaise
dfunny | Doug Yancey Funny
(2 rows)
SELECT * FROM person_audit ORDER BY effective_at;
login_name | display_name | operation | effective_at | userid
------------+-------------------+-----------+----------------------------+----------
dfunny | Doug Funny | INSERT | 2018-05-26 18:48:07.6903 | postgres
pmayo | Patti Mayonnaise | INSERT | 2018-05-26 18:48:07.698623 | postgres
dfunny | Doug Yancey Funny | UPDATE | 2018-05-26 18:48:07.707284 | postgres
(3 rows)
最後に、削除機能を実行し、監査テーブルにそのレコードも含まれていることを確認します。
DELETE FROM person WHERE login_name = 'pmayo';
SELECT * FROM person;
login_name | display_name
------------+-------------------
dfunny | Doug Yancey Funny
(1 row)
SELECT * FROM person_audit ORDER BY effective_at;
login_name | display_name | operation | effective_at | userid
------------+-------------------+-----------+----------------------------+----------
dfunny | Doug Funny | INSERT | 2018-05-27 08:13:22.747226 | postgres
pmayo | Patti Mayonnaise | INSERT | 2018-05-27 08:13:22.74839 | postgres
dfunny | Doug Yancey Funny | UPDATE | 2018-05-27 08:13:22.749495 | postgres
pmayo | Patti Mayonnaise | DELETE | 2018-05-27 08:13:22.753425 | postgres
(4 rows)
例2-派生値
これをさらに一歩進めて、各行に自由形式のテキストドキュメント、たとえばプレーンテキスト形式の履歴書、会議用ペーパー、エンターテインメント文字の要約を保存し、強力な全文検索の使用をサポートしたいとします。これらの自由形式のテキストドキュメントに対するPostgreSQLの機能。
最初に、ドキュメントと関連するテキスト検索ベクトルの保存をサポートする2つの属性をプリンシパルテーブルに追加します。テキスト検索ベクトルは行ごとに導出されるため、監査テーブルに保存しても意味がありません。関連する監査テーブルにドキュメント保存列を追加します。
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;
ALTER TABLE person_audit ADD COLUMN abstract TEXT;
次に、これらの新しい属性を処理するようにトリガー関数を変更します。プレーンテキスト列は他のユーザー入力データと同じ方法で処理されますが、テキスト検索ベクトルは派生値であるため、効率的な検索のためにドキュメントテキストをtsvectorデータ型に変換する関数呼び出しによって処理されます。
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
LANGUAGE plpgsql
SET SCHEMA 'public'
AS $$
BEGIN
IF LENGTH(NEW.login_name) = 0 THEN
RAISE EXCEPTION 'Login name must not be empty.';
END IF;
IF POSITION(' ' IN NEW.login_name) > 0 THEN
RAISE EXCEPTION 'Login name must not include white space.';
END IF;
-- Modified audit code to include text abstract
INSERT INTO person_audit (login_name, display_name, operation, abstract)
VALUES (NEW.login_name, NEW.display_name, TG_OP, NEW.abstract);
-- New code to reduce text to text-search vector
SELECT to_tsvector(NEW.abstract) INTO NEW.ts_abstract;
RETURN NEW;
END;
$$;
テストとして、既存の行をWikipediaの詳細テキストで更新します:
UPDATE person SET abstract = 'Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd.' WHERE login_name = 'dfunny';
次に、テキスト検索ベクトル処理が成功したことを確認します。
SELECT login_name, ts_abstract FROM person;
login_name | ts_abstract
------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
dfunny | '11':11 '12':13 'an':5 'and':9 'as':4 'boy':16 'crowd':24 'depicted':3 'doug':1 'fit':20 'gullible':10 'in':21 'insecure':8 'introverted':6 'is':2 'later':12 'old':15 'quiet':7 'the':23 'to':19 'wants':18 'who':17 'with':22 'year':14
(1 row)
例3-トリガーとビュー
上記の例から派生したテキスト検索ベクトルは、人間による消費を目的としたものではありません。つまり、ユーザーが入力するものではなく、エンドユーザーに値を提示することは期待できません。ユーザーがts_abstract列に値を挿入しようとすると、提供されたものはすべて破棄され、トリガー関数の内部で派生した値に置き換えられるため、検索コーパスのポイズニングから保護されます。列を完全に非表示にするために、その属性を含まない簡略化されたビューを定義できますが、基になるテーブルでのトリガーアクティビティの利点は引き続き得られます:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;
簡単なビューの場合、PostgreSQLは自動的に書き込み可能にするため、データを正常に挿入または更新するために他に何もする必要はありません。 DMLが基になるテーブルで有効になると、ステートメントがテーブルに直接適用されたかのようにトリガーがアクティブ化されるため、personテーブルの検索ベクトル列に入力するバックグラウンドで実行されるテキスト検索サポートと、情報を監査テーブルに変更します:
INSERT INTO abridged_person VALUES ('skeeter', 'Mosquito Valentine', 'Skeeter is Doug''s best friend. He is famous in both series for the honking sounds he frequently makes.');
SELECT login_name, ts_abstract FROM person WHERE login_name = 'skeeter';
login_name | ts_abstract
------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
skeeter | 'best':5 'both':11 'doug':3 'famous':9 'for':13 'frequently':18 'friend':6 'he':7,17 'honking':15 'in':10 'is':2,8 'makes':19 's':4 'series':12 'skeeter':1 'sounds':16 'the':14
(1 row)
SELECT login_name, display_name, operation, userid FROM person_audit ORDER BY effective_at;
login_name | display_name | operation | userid
------------+--------------------+-----------+----------
dfunny | Doug Funny | INSERT | postgres
pmayo | Patti Mayonnaise | INSERT | postgres
dfunny | Doug Yancey Funny | UPDATE | postgres
pmayo | Patti Mayonnaise | DELETE | postgres
dfunny | Doug Yancey Funny | UPDATE | postgres
skeeter | Mosquito Valentine | INSERT | postgres
(6 rows)
自動的に書き込み可能であるための要件を満たさない、より複雑なビューの場合は、ルールシステムまたは代わりに トリガーは、書き込みと削除をサポートする役割を果たします。
例4-要約値
さらに装飾して、ある種のトランザクションテーブルがあるシナリオを扱いましょう。これは、労働時間の記録、倉庫または小売在庫の在庫の追加と削減、または各個人の借方と貸方が記載された小切手帳である可能性があります。
CREATE TABLE transaction (
login_name character varying(9) NOT NULL,
post_date date,
description character varying,
debit money,
credit money,
FOREIGN KEY (login_name) REFERENCES person (login_name)
);
また、トランザクション履歴を保持することは重要ですが、ビジネスルールでは、トランザクションの詳細ではなく、アプリケーション処理でネットバランスを使用する必要があるとします。残高が必要になるたびにすべてのトランザクションを合計して残高を頻繁に再計算する必要がないように、新しい列を追加し、トリガーとストアド関数を使用して維持することで、personテーブルの現在の残高値を非正規化して保持できます。トランザクションが挿入されたときの正味残高:
ALTER TABLE person ADD COLUMN balance MONEY DEFAULT 0;
CREATE FUNCTION transaction_bit() RETURNS trigger
LANGUAGE plpgsql
SET SCHEMA 'public'
AS $$
DECLARE
newbalance money;
BEGIN
-- Update person account balance
UPDATE person
SET balance =
balance +
COALESCE(NEW.debit, 0::money) -
COALESCE(NEW.credit, 0::money)
WHERE login_name = NEW.login_name
RETURNING balance INTO newbalance;
-- Data validation
IF COALESCE(NEW.debit, 0::money) < 0::money THEN
RAISE EXCEPTION 'Debit value must be non-negative';
END IF;
IF COALESCE(NEW.credit, 0::money) < 0::money THEN
RAISE EXCEPTION 'Credit value must be non-negative';
END IF;
IF newbalance < 0::money THEN
RAISE EXCEPTION 'Insufficient funds: %', NEW;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER transaction_bit
BEFORE INSERT ON transaction
FOR EACH ROW EXECUTE PROCEDURE transaction_bit();
借方、貸方、および残高の値の非負性を検証する前に、ストアド関数で最初に更新を行うのは奇妙に思えるかもしれませんが、データ検証の観点からは、トリガー関数の本体がとして実行されるため、順序は重要ではありません。データベーストランザクション。したがって、これらの検証チェックが失敗した場合、例外が発生したときにトランザクション全体がロールバックされます。最初に更新を実行する利点は、トランザクションの期間中、更新によって影響を受ける行がロックされるため、現在のトランザクションが完了するまで、同じ行を更新しようとする他のセッションがブロックされることです。さらなる検証テストにより、結果のバランスが負ではないことが保証され、例外情報メッセージに変数を含めることができます。この場合、デバッグのために、問題のある試行された挿入トランザクション行が返されます。
実際に機能することを示すために、いくつかのサンプルエントリと、各ステップで更新された残高を示すチェックを示します。
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+---------
dfunny | $0.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-11', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$2780.52', NULL);
ERROR: Insufficient funds: (dfunny,2018-01-17,"FOR:BGE PAYMENT ACH Withdrawal",,"$2,780.52")
上記のトランザクションが不十分な資金でどのように失敗するか、つまり、マイナスの残高が生成され、正常にロールバックされることに注意してください。また、デバッグ用のエラーメッセージの追加の詳細として、NEW特殊変数を含む行全体を返したことにも注意してください。
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$278.52', NULL);
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $1,721.48
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal', '$35.29', NULL);
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $1,686.19
(1 row)
例5-トリガーとビューのRedux
ただし、上記の実装には問題があります。それは、悪意のあるユーザーがお金を印刷することを妨げるものは何もないということです。
BEGIN;
UPDATE person SET balance = '1000000000.00';
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
login_name | balance
------------+-------------------
dfunny | $1,000,000,000.00
(1 row)
ROLLBACK;
今のところ、上記の盗難をロールバックし、ビューのトリガーを使用して残高値の更新を防ぐことにより、保護を組み込む方法を示します。
最初に、前の簡略化されたビューを拡張して、バランス列を表示します。
CREATE OR REPLACE VIEW abridged_person AS
SELECT login_name, display_name, abstract, balance FROM person;
これにより、天びんへの読み取りアクセスが明らかに許可されますが、単一のテーブルに基づくこのような単純なビューの場合、PostgreSQLは自動的にビューを書き込み可能にするため、問題は解決しません。
BEGIN;
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
login_name | balance
------------+-------------------
dfunny | $1,000,000,000.00
(1 row)
ROLLBACK;
We could use a rule, but to illustrate that triggers can be defined on views as well as tables, we will take the latter route and use an instead of update trigger on the view to block unwanted DML, preventing non-transactional changes to the balance value:
CREATE FUNCTION abridged_person_iut() RETURNS TRIGGER
LANGUAGE plpgsql
SET search_path TO public
AS $$
BEGIN
-- Disallow non-transactional changes to balance
NEW.balance = OLD.balance;
RETURN NEW;
END;
$$;
CREATE TRIGGER abridged_person_iut
INSTEAD OF UPDATE ON abridged_person
FOR EACH ROW EXECUTE PROCEDURE abridged_person_iut();
The above instead of update trigger and stored procedure discards any attempted updates to the balance value and instead forces use of the value present in the database prior to the triggering update statement:
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $1,686.19
(1 row)
which affords protection against un-auditable changes to the balance value.
今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードするEXAMPLE 6 - Elevated Privileges
So far all the example code above has been executed at the database owner level by the postgres login role, so any of our anti-tampering efforts could be obviated… that’s just a fact of the database owner super-user privileges.
Our final example illustrates how triggers and stored functions can be used to allow the execution of code by a non-privileged user at a higher privilege than the logged in session user normally has by employing the SECURITY DEFINER attribute associated with stored functions.
First, we define a non-privileged login role, eve and confirm that upon instantiation there are no privileges:
CREATE USER eve;
\dp
Access privileges
Schema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------------+-------+-------------------+-------------------+----------
public | abridged_person | view | | |
public | person | table | | |
public | person_audit | table | | |
public | transaction | table | | |
(4 rows)
We grant read, update, and create privileges on the abridged person view and read and create to the transaction table:
GRANT SELECT,INSERT, UPDATE ON abridged_person TO eve;
GRANT SELECT,INSERT ON transaction TO eve;
\dp
Access privileges
Schema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------------+-------+---------------------------+-------------------+----------
public | abridged_person | view | postgres=arwdDxt/postgres+| |
| | | eve=arw/postgres | |
public | person | table | | |
public | person_audit | table | | |
public | transaction | table | postgres=arwdDxt/postgres+| |
| | | eve=ar/postgres | |
(4 rows)
By way of confirmation we see that eve is denied access to the person and person_audit tables:
SET SESSION AUTHORIZATION eve;
SELECT * FROM person;
ERROR: permission denied for relation person
SELECT * from person_audit;
ERROR: permission denied for relation person_audit
and that she does have appropriate read access to the abridged_person and transaction tables:
SELECT * FROM abridged_person;
login_name | display_name | abstract | balance
------------+--------------------+---------------------------------------------------------------------------------------------------------------------------------+-----------
skeeter | Mosquito Valentine | Skeeter is Doug's best friend. He is famous in both series for the honking sounds he frequently makes. | $0.00
dfunny | Doug Yancey Funny | Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd. | $1,686.19
(2 rows)
SELECT * FROM transaction;
login_name | post_date | description | debit | credit
------------+------------+--------------------------------------------------------------+-----------+---------
dfunny | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |
dfunny | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal | | $278.52
dfunny | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal | | $35.29
(3 rows)
However, even though she has write privilege on the transaction table, a transaction insert attempt fails due to lack of privilege on the person テーブル。
SET SESSION AUTHORIZATION eve;
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR: permission denied for relation person
CONTEXT: SQL statement "UPDATE person
SET balance =
balance +
COALESCE(NEW.debit, 0::money) -
COALESCE(NEW.credit, 0::money)
WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement
The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:
RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;
SET SESSION AUTHORIZATION eve;
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
SELECT * FROM transaction;
login_name | post_date | description | debit | credit
------------+------------+--------------------------------------------------------------+-----------+---------
dfunny | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |
dfunny | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal | | $278.52
dfunny | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal | | $35.29
dfunny | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |
(4 rows)
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
login_name | balance
------------+-----------
dfunny | $3,686.19
(1 row)
Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.
結論
As lengthy as this article is, there’s still a lot more to say about triggers and stored functions. What we covered here is a basic introduction with a consideration of pros and cons of triggers and stored functions. We illustrated six use-case examples showing data validation, change logging, deriving values from inserted data, data hiding with simple updatable views, maintaining summary data in separate tables, and allowing safe invocation of code at elevated privilege. Look for a future article on using triggers and stored functions to prevent missing values in sequentially-incrementing (serial) columns.