用于根据计数限制插入的通用触发器

时间:2012-06-26 00:07:40

标签: postgresql triggers insert many-to-many plpgsql

背景

在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_address允许1024 cookie个值;然而,在tag_comment表中,每个comment_id可以有10个关联tag_name_id s。

问题

目前,许多功能都对这些限制进行了硬编码;在整个数据库中分散限制,并防止它们动态更改。

问题

您如何以通用方式对表施加最大的多对多关系限制?

创建一个表来跟踪限制:

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();

显然它不会像书面那样工作......有没有办法让它发挥作用,或以其他方式强制执行以下限制:

  • 在一个地方;和
  • 没有硬编码?

谢谢!

3 个答案:

答案 0 :(得分:1)

我一直在做类似的通用触发器。 最棘手的部分是根据列名称在NEW记录中获取值条目。

我是按照以下方式做的:

  • NEW数据转换为数组;
  • 找到列的attnum并将其用作数组的索引。

只要数据中没有逗号,此方法就可以运行 :(我不知道如何将NEWOLD变量转换为其他方式价值数组。

以下功能可能有所帮助:

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;

此函数将检查为给定表定义的所有条件。

答案 1 :(得分:0)

是的,有办法让它发挥作用。

在我个人看来,你的想法是要走的路。它只需要一个级别的“元”。因此,表imposed_restrictions应该具有触发器,其被激活after insertupdatedelete。然后代码应该创建,修改或删除触发器和函数。

看一下execute statement of PL/pgSQL,它本质上允许你执行任何字符串。不用说,这个字符串可能包含触发器,函数等的定义。显然,你可以在触发器中访问OLDNEW,这样你就可以填充字符串中的占位符了。完成了。

我相信你应该能够用这个答案完成你想要的。请注意,这是我对该主题的个人观点,它可能不是最佳解决方案 - 我希望看到一种不同的,也许更有效的方法。

修改 - 以下是我项目之一的示例。它位于被触发的函数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;

答案 2 :(得分: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