4

バックグラウンド

PostgreSQL 9.0 データベースには、多対多の関係を持つさまざまなテーブルがあります。これらの関係の数は制限する必要があります。いくつかのテーブルの例を次に示します。

CREATE TABLE authentication (
  id bigserial NOT NULL, -- Primary key
  cookie character varying(64) NOT NULL, -- Authenticates the user with a cookie
  ip_address character varying(40) NOT NULL -- Device IP address (IPv6-friendly)
)

CREATE TABLE tag_comment (
  id bigserial NOT NULL, -- Primary key
  comment_id bigint, -- Foreign key to the comment table
  tag_name_id bigint -- Foreign key to the tag name table
)

ただし、関係が異なれば、制限も異なります。たとえば、authentication表では、ip_address1024 個のcookie値が許可されています。一方、tag_commentテーブルでは、それぞれcomment_idに 10 個の を関連付けることができますtag_name_id

問題

現在、多くの関数にこれらの制限がハードコーディングされています。データベース全体に制限を分散させ、動的に変更されないようにします。

質問

一般的な方法でテーブルに最大の多対多の関係制限を課すにはどうすればよいでしょうか?

考え

制限を追跡するテーブルを作成します。

CREATE TABLE imposed_maximums (
  id serial NOT NULL,
  table_name  character varying(128) NOT NULL,
  column_group character varying(128) NOT NULL,
  column_count character varying(128) NOT NULL,
  max_size INTEGER
)

制限を確立します。

INSERT INTO imposed_maximums
  (table_name, column_group, column_count, max_size) VALUES
  ('authentication', 'ip_address', 'cookie', 1024);
INSERT INTO imposed_maximums
  (table_name, column_group, column_count, max_size) VALUES
  ('tag_comment', 'comment_id', 'tag_id', 10);

トリガー関数を作成します。

CREATE OR REPLACE FUNCTION impose_maximum()
  RETURNS trigger AS
$BODY$
BEGIN
  -- Join this up with imposed_maximums somehow?
  select
    count(1)
  from
    -- the table name
  where
    -- the group column = NEW value to INSERT;

  RETURN NEW;
END;

トリガーをすべてのテーブルにアタッチします。

CREATE TRIGGER trigger_authentication_impose_maximum
  BEFORE INSERT
  ON authentication
  FOR EACH ROW
  EXECUTE PROCEDURE impose_maximum();

明らかに、書かれているようには機能しません...それを機能させる方法、または次のような制限を適用する方法はありますか:

  • 単一の場所で; と
  • ハードコーディングされていませんか?

ありがとうございました!

4

3 に答える 3

1

私は同様のタイプの一般的なトリガーを行ってきました。最も難しい部分はNEW、列名に基づいてレコードの値エントリを取得することです。

私は次の方法でそれをやっています:

  • NEWデータを配列に変換します。
  • 列の を見つけてattnum、配列のインデックスとして使用します。

このアプローチは、データにコンマがない限り機能します:(変数を値の配列に変換NEWする他の方法はわかりません。OLD

次の関数が役立つ場合があります。

CREATE OR REPLACE FUNCTION impose_maximum() RETURNS trigger AS $impose_maximum$
DECLARE
  _sql  text;
  _cnt  int8;
  _vals text[];
  _anum int4;
  _im   record;

BEGIN
 _vals := string_to_array(translate(trim(NEW::text), '()', ''), ',');

 FOR _im IN SELECT * FROM imposed_maximums WHERE table_name = TG_TABLE_NAME LOOP
  SELECT attnum INTO _anum FROM pg_catalog.pg_attribute a
    JOIN pg_catalog.pg_class t ON t.oid = a.attrelid
   WHERE t.relkind = 'r' AND t.relname = TG_TABLE_NAME
     AND NOT a.attisdropped AND a.attname = _im.column_group;

  _sql := 'SELECT count('||quote_ident(_im.column_count)||')'||
          ' FROM '||quote_ident(_im.table_name)||
          ' WHERE '||quote_ident(_im.column_group)||' = $1';

  EXECUTE _sql INTO _cnt USING _vals[_anum];

  IF _cnt > CAST(_im.max_size AS int8) THEN
    RAISE EXCEPTION 'Maximum of % hit for column % in table %(%=%)',
      _im.max_size, _im.column_count,
      _im.table_name, _im.column_group, _vals[_anum];
  END IF;
 END LOOP;

 RETURN NEW;
END; $impose_maximum$ LANGUAGE plpgsql;

この関数は、特定のテーブルに対して定義されたすべての条件をチェックします。

于 2012-06-26T14:39:09.573 に答える
0

これらの関数 + トリガーは、テンプレートとして使用できます。それらを @Sorrow の関数とトリガーを動的に生成する手法と組み合わせると、OPの問題を解決できます。影響を受けるすべての行のカウントを (COUNT() 集計関数を呼び出して) 再計算する代わりに、「増分」カウントを維持することに注意してください。これは安くなるはずです。

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path='tmp';

CREATE TABLE authentication
  ( id bigserial NOT NULL -- Primary key
  , cookie varchar(64) NOT NULL -- Authenticates the user with a cookie
  , ip_address varchar(40) NOT NULL -- Device IP address (IPv6-friendly)
  , PRIMARY KEY (ip_address, cookie)
);

CREATE TABLE authentication_ip_count (
    ip_address character varying(40) NOT NULL
      PRIMARY KEY -- REFERENCES authentication(ip_address)
    , refcnt INTEGER NOT NULL DEFAULT 0
    --
    -- This is much easyer:
    --  keep the max value inside the table
    --        + use a table constraint
    -- , maxcnt INTEGER NOT NULL DEFAULT 2 -- actually 100
    -- , CONSTRAINT no_more_cookies CHECK (refcnt <= maxcnt)
        );

CREATE TABLE imposed_maxima
  ( id serial NOT NULL
  , table_name  varchar NOT NULL
  , column_group varchar NOT NULL
  , column_count varchar NOT NULL
  , max_size INTEGER NOT NULL
  , PRIMARY KEY (table_name,column_group,column_count)
);
INSERT INTO imposed_maxima(table_name,column_group,column_count,max_size)
              VALUES('authentication','ip_address','cookie', 2);

CREATE OR REPLACE FUNCTION authentication_impose_maximum()
  RETURNS trigger AS
$BODY$
DECLARE
        dummy INTEGER;
BEGIN
  IF (TG_OP = 'INSERT') THEN
        INSERT INTO authentication_ip_count (ip_address)
        SELECT sq.*
        FROM ( SELECT NEW.ip_address) sq
        WHERE NOT EXISTS (
                SELECT *
                FROM authentication_ip_count nx
                WHERE nx.ip_address = sq.ip_address
                );

        UPDATE authentication_ip_count
        SET refcnt = refcnt + 1
        WHERE ip_address = NEW.ip_address
                ;
        SELECT COUNT(*) into dummy -- ac.refcnt, mx.max_size
        FROM authentication_ip_count ac
        JOIN imposed_maxima mx ON (1=1) -- outer join
        WHERE ac.ip_address =  NEW.ip_address
        AND mx.table_name  = 'authentication'
        AND mx.column_group = 'ip_address'
        AND mx.column_count = 'cookie'
        AND ac.refcnt > mx.max_size
                ;
        IF FOUND AND dummy > 0 THEN
                RAISE EXCEPTION 'Cookie moster detected';
        END IF;


  ELSIF (TG_OP = 'DELETE') THEN

        UPDATE authentication_ip_count
        SET refcnt = refcnt - 1
        WHERE ip_address = OLD.ip_address
                ;
        DELETE FROM authentication_ip_count ac
        WHERE ac.ip_address = OLD.ip_address
        AND ac.refcnt <= 0
                ;
  -- ELSIF (TG_OP = 'UPDATE') THEN
  -- (Only needed if we allow updates of ip-address)
  -- otherwise the count stays the same.

  END IF;

  RETURN NEW;

END;

$BODY$
  LANGUAGE plpgsql;

CREATE TRIGGER trigger_authentication_impose_maximum
  BEFORE INSERT OR UPDATE OR DELETE
  ON authentication
  FOR EACH ROW
  EXECUTE PROCEDURE authentication_impose_maximum();

        -- Test it ...
INSERT INTO authentication(ip_address, cookie) VALUES ('1.2.3.4', 'Some koekje' );
INSERT INTO authentication(ip_address, cookie) VALUES ('1.2.3.4', 'kaakje' );
INSERT INTO authentication(ip_address, cookie) VALUES ('1.2.3.4', 'Yet another cookie' );

結果:

INSERT 0 1
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
INSERT 0 1
ERROR:  Cookie moster detected
于 2012-06-26T15:13:59.460 に答える
0

はい、それを機能させる方法があります。

私の個人的な意見では、あなたのアイデアは進むべき道です。1 レベルの「メタ」が必要なだけです。したがって、テーブルにはimposed_restrictionsトリガーが必要after insertです。次に、コードはトリガーと関数を作成、変更、または削除する必要があります。updatedelete

executePL/pgSQL のステートメントを見てください。これにより、本質的に、任意の文字列を実行できます。言うまでもなく、この文字列にはトリガーや関数などの定義が含まれる場合があります。明らかに、トリガーへのアクセス権とOLDトリガーへのアクセス権NEWがあるため、文字列にプレースホルダーを入力すれば完了です。

この答えであなたが望むことを達成できるはずだと私は信じています。これはこのトピックに関する私の個人的な見解であり、最適な解決策ではない可能性があることに注意してください.別の、おそらくより効率的なアプローチを望んでいます.

編集- 以下は、私の古いプロジェクトの 1 つからのサンプルです。これは、トリガーされる関数内にありますbefore update(今思いついたのですが、おそらく呼び出されたはずafterです ;) そして、コードは乱雑です$escape$。あの頃は本当に若かった。それにもかかわらず、切り取られたものは、あなたが望むものを達成することが可能であることを示しています.

query:=''CREATE FUNCTION '' || NEW.function_name || ''('';
IF NEW.parameter=''t'' THEN
  query:=query || ''integer'';
END IF;
query:=query || '') RETURNS setof '' || type_name || '' AS'' || chr(39);
query:=query || '' DECLARE list '' || type_name || ''; '';
query:=query || ''BEGIN '';
query:=query || '' FOR list IN EXECUTE '' || chr(39) || chr(39);
query:=query || temp_s || '' FROM '' || NEW.table_name;
IF NEW.parameter=''t'' THEN
  query:=query || '' WHERE id='' || chr(39) || chr(39) || ''||'' ||  chr(36) || ''1'';
ELSE
  query:=query || '';'' || chr(39) || chr(39);
END IF;
query:=query || '' LOOP  RETURN NEXT list; '';
query:=query || ''END LOOP; RETURN; END; '' || chr(39);
query:=query || ''LANGUAGE '' || chr(39) || ''plpgsql'' || chr(39) || '';'';
EXECUTE query;
于 2012-06-26T12:26:48.147 に答える