web-dev-qa-db-ja.com

シリアル列への明示的な挿入を防ぐ

シリアル列への明示的な挿入を防ぎたい。私は次のトリガーに達しました:

drop table test_table;

create table test_table(
id bigserial primary key,
foobar text
);

create or replace function serial_id_check() returns trigger as
$$
begin
    if new.id != currval(TG_TABLE_NAME||'_id_seq') then
        raise exception 'Explicit insert into serial id, currval = %, tried to insert = %', currval(TG_TABLE_NAME||'_id_seq'), new.id;
    end if;
    return new;
end;
$$ language plpgsql;

create trigger test_table_serial_id_check
before insert on test_table
for each row
execute procedure serial_id_check();

多分もっと良いアプローチがありますか?たぶんこのアプローチは壊れており、これはまったく達成できませんか?

追伸また、挿入と更新の権限を付与せず、挿入/更新のためのpgplsqlプロシージャのみを許可することも考えています。しかし、現在のところ、このアプローチは不可能です。

3
Ivan Sopov

更新可能なビュー

Postgres 9.4では、ビューは自動的に更新可能です列ごと。つまり、基本的な条件が満たされている限り、列は基になる列への単純な参照のために自動的に更新可能です。式はそうではありません。この機能を利用できます。

_CREATE TABLE test_table(
  id serial PRIMARY KEY
, foobar text
);

CREATE VIEW test_view AS
SELECT id * 1 AS id  -- id unchanged but not updatable!
     , foobar
FROM   test_table;
_

idと_1_を乗算すると、値は変更されませんが、列は自動的に更新できなくなります。

  • ユーザーからの_test_table_に対するREVOKEINSERT/UPDATE特権。
  • それらの特権ownを保持する別のロールをビューに作成します。
  • _GRANT INSERT_の_test_view_/UPDATEをユーザーに提供します。

今ではすべてを行うことができますが、idの値を手動で設定または変更することはできません。 DELETEの処理方法はユーザーが選択します。

このシンプルで迅速なソリューションは、Postgres 9.4ではそのまま使用できます。自動更新可能なビューはPostgres 9.3で導入されましたが、機能が動作するためには、そのバージョンですべての列が更新可能である必要があります。

Postgres 9.3以前で、_INSTEAD OF INSERT_トリガーまたは無条件_ON INSERT DO INSTEAD_ルールを提供します。

ルール/トリガーを手動で記述している間、_id * 1_トリックは必要ありません。 Postgres 9.4以降でもこれを使用して機能を微調整することができます。例:

_CREATE VIEW test_view1 AS TABLE test_table;

CREATE OR REPLACE RULE ins_up AS
 ON INSERT TO test_view1 DO INSTEAD
INSERT INTO test_table (foobar) VALUES (NEW.foobar);
_

現在、ビューでINSERTを使用できますが、UPDATEまたはDELETEはまだ使用できません。必要に応じて、さらにルールを記述します。

SQLフィドル (Postgres 9.6)

重要difference:自動的に更新可能なビューは、INSERT/UPDATEの値への試みを拒否しますが、idexceptionを使用すると、デモンストレーションされたRULEは単にidの-​​ignores値であり、例外なく続行されます。

単純なトリガー

または、シリアル列をシーケンスの次の値無条件で単純に上書きすることもできます。

_CREATE OR REPLACE FUNCTION force_serial_id()
  RETURNS trigger AS
$func$
BEGIN
   NEW.id := nextval(pg_get_serial_sequence(quote_ident(TG_TABLE_NAME), 'id'));
   RETURN NEW;
END
$func$ LANGUAGE plpgsql;
_

これは、スマートにしようとするよりも簡単で安価で、エラーが発生しにくくなります。 quote_ident() 不正な名前を安全にエスケープします(SQLインジェクションからも保護されます)。

@ dezso commented のように、これは通常の操作でtwoserial列を使用して行ごとに数値を書き込みます。デフォルトがフェッチされるためですbeforeトリガー関数が起動します。通常、シーケンスのギャップは(とにかく予想される)問題ではありませんが、列からDEFAULTを削除することで副作用を回避できます。次に、トリガーのみに依存します。

別のトリガーとトリガー自体の条件WHEN (OLD.id <> NEW.id)を使用してUPDATEケースを微調整できます。構文例:

pg_get_serial_sequence() の使用に注意してください。これは、非基本的な識別子の元のように壊れません。 _"MyTable"_またはデフォルト以外のシーケンス名を考えてください。列名はidと仮定しますが、私は個人的にneverを使用しています。

12

ID列

これは正確ではない答えまだではありませんが、あなたが望むのはGENERATED ALWAYS。そして、それはその途中です、おそらくPostgreSQLの次のリリースであるPostgreSQL 10

CREATE TABLE itest4 (
  a int GENERATED ALWAYS AS IDENTITY,
  b text
);

パッチに関するメール から構文を盗んだ動詞。

ここのcommitfestの問題 を追跡します。

SQL仕様から、

If <identity column specification> is specified, then:
i) An indication that the column is an identity column.
ii) If ALWAYS is specified, then an indication that values are always generated.
iii) If BY DEFAULT is specified, then an indication that values are generated by default.
iv) The General Rules of Subclause 9.26, “Creation of a sequence generator”, are applied with
SGO as OPTIONS and ICT as DATA TYPE; let the descriptor of the sequence generator SG be
the SEQGENDESC returned from the application of those General Rules.
4
Evan Carroll

承認された回答はすべて問題ありませんが、どこかからのコードが明示的なID値を挿入し、それによってシーケンスを途中で壊すように見えるシナリオをトラブルシューティングしようとする場合、単に問題を修正するのではなく、問題のコードにエラーを発生させたいと思います。その場での問題(またはビューを使用するためにすべてを書き換える)。

OP独自のソリューションは、INSERT INTO table (col2) VALUES ('val'), ('val')またはINSERT INTO table (col2) SELECT somefield FROM anothertableを使用して複数の挿入を行うシナリオでは機能しません。

トリガー条件に対するこのわずかな修正により、この問題が修正されます。また、シーケンスの名前をより確実に特定するためのpg_get_serial_sequenceの使用にも注意してください。 (pg_get_serial_sequenceは、シーケンスのOWNED BYプロパティが正しく設定されていることに依存していることに注意してください。)

create or replace function serial_id_check() returns trigger as
$$
begin
    if new.id > currval(pg_get_serial_sequence(TG_TABLE_NAME, 'id')) then
        raise exception 'Explicit insert into serial id, currval = %, tried to insert = %', currval(pg_get_serial_sequence(TG_TABLE_NAME, 'id')), new.id;
    end if;
    return new;
end;
$$ language plpgsql;
0
Richard