Postgresql动态函数,带有当前表名

问题描述 投票:0回答:2

我有一个函数(audit.create_audit_table())接受一个表名数组。它创建一个函数audit.if_modified_func(),然后遍历每个表名并创建一个审计表并将触发器应用于主表。该函数编译并创建没有错误。当我运行该功能

select audit.create_audit_table(ARRAY['organization'])

我一直得到以下错误,我不知道为什么因为我认为TG_TABLE_NAME是一个自动变量,它将让我访问正在执行audit.if_modified_func()的当前表

错误:

ERROR:  column "tg_table_name" does not exist
LINE 3:   audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
                                           ^

这是功能:

    CREATE OR REPLACE FUNCTION audit.create_audit_table(table_names character varying[])
RETURNS character varying AS
$BODY$
    DECLARE
    table_name varchar;
    i int;
BEGIN
    EXECUTE 'CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
    DECLARE     
        audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
        include_values boolean;
        log_diffs boolean;
        h_old hstore;
        h_new hstore;
        excluded_cols text[] = ARRAY[]::text[];
    BEGIN
        IF TG_WHEN <> ''AFTER'' THEN
            RAISE EXCEPTION ''audit.if_modified_func() may only run as an AFTER trigger'';
        END IF;

        audit_row = ROW(
            nextval(''audit.'|| quote_ident(TG_TABLE_NAME::text) ||'_event_id_seq''), -- event_id
            TG_TABLE_SCHEMA::text,                        -- schema_name
            TG_TABLE_NAME::text,                          -- table_name
            TG_RELID,                                     -- relation OID for much quicker searches
            session_user::text,                           -- session_user_name
            current_timestamp,                            -- action_tstamp_tx
            statement_timestamp(),                        -- action_tstamp_stm
            clock_timestamp(),                            -- action_tstamp_clk
            txid_current(),                               -- transaction ID
            current_setting(''application_name''),          -- client application
            inet_client_addr(),                           -- client_addr
            inet_client_port(),                           -- client_port
            current_query(),                              -- top-level query or queries (if multistatement) from client
            substring(TG_OP,1,1),                         -- action
            NULL, NULL,                                   -- row_data, changed_fields
            ''f''                                           -- statement_only
            );

        IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM ''f''::boolean THEN
            audit_row.client_query = NULL;
        END IF;

        IF TG_ARGV[1] IS NOT NULL THEN
            excluded_cols = TG_ARGV[1]::text[];
        END IF;

        IF (TG_OP = ''UPDATE'' AND TG_LEVEL = ''ROW'') THEN
            audit_row.row_data = hstore(OLD.*) - excluded_cols;
            audit_row.changed_fields =  (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
            IF audit_row.changed_fields = hstore('''') THEN
                -- All changed fields are ignored. Skip this update.
                RETURN NULL;
            END IF;
        ELSIF (TG_OP = ''DELETE'' AND TG_LEVEL = ''ROW'') THEN
            audit_row.row_data = hstore(OLD.*) - excluded_cols;
        ELSIF (TG_OP = ''INSERT'' AND TG_LEVEL = ''ROW'') THEN
            audit_row.row_data = hstore(NEW.*) - excluded_cols;
        ELSIF (TG_LEVEL = ''STATEMENT'' AND TG_OP IN (''INSERT'',''UPDATE'',''DELETE'',''TRUNCATE'')) THEN
            audit_row.statement_only = ''t'';
        ELSE
            RAISE EXCEPTION ''[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %%, %%'',TG_OP, TG_LEVEL;
            RETURN NULL;
        END IF;
        INSERT INTO audit.'|| quote_ident(TG_TABLE_NAME::TEXT) ||' VALUES (audit_row.*);
    RETURN null;
    END;
    $$
    LANGUAGE plpgsql;   
    ALTER FUNCTION audit.if_modified_func()
    OWNER TO postgres;';

    FOR i in 1..array_upper(table_names, 1) LOOP

        EXECUTE format('
        DROP TABLE IF EXISTS audit.%1$s;
        CREATE TABLE audit.%1$s (
        event_id bigserial primary key,
        schema_name text not null,
        table_name text not null,
        relid oid not null,
        session_user_name text,
        action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
        action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
        action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
        transaction_id bigint,
        application_name text,
        client_addr inet,
        client_port integer,
        client_query text,
        action TEXT NOT NULL CHECK (action IN (''I'',''D'',''U'', ''T'')),
        row_data hstore,
        changed_fields hstore,
        statement_only boolean not null
        );

        REVOKE ALL ON audit.%1$s FROM public;

        COMMENT ON TABLE audit.%1$s IS ''History of auditable actions on audited tables, from audit.if_modified_func()'';
        COMMENT ON COLUMN audit.%1$s.event_id IS ''Unique identifier for each auditable event'';
        COMMENT ON COLUMN audit.%1$s.schema_name IS ''Database schema audited table for this event is in'';
        COMMENT ON COLUMN audit.%1$s.table_name IS ''Non-schema-qualified table name of table event occured in'';
        COMMENT ON COLUMN audit.%1$s.relid IS ''Table OID. Changes with drop/create. Get with ''''tablename''''::regclass'';
        COMMENT ON COLUMN audit.%1$s.session_user_name IS ''Login / session user whose statement caused the audited event'';
        COMMENT ON COLUMN audit.%1$s.action_tstamp_tx IS ''Transaction start timestamp for tx in which audited event occurred'';
        COMMENT ON COLUMN audit.%1$s.action_tstamp_stm IS ''Statement start timestamp for tx in which audited event occurred'';
        COMMENT ON COLUMN audit.%1$s.action_tstamp_clk IS ''Wall clock time at which audited event''''s trigger call occurred'';
        COMMENT ON COLUMN audit.%1$s.transaction_id IS ''Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.'';
        COMMENT ON COLUMN audit.%1$s.client_addr IS ''IP address of client that issued query. Null for unix domain socket.'';
        COMMENT ON COLUMN audit.%1$s.client_port IS ''Remote peer IP port address of client that issued query. Undefined for unix socket.'';
        COMMENT ON COLUMN audit.%1$s.client_query IS ''Top-level query that caused this auditable event. May be more than one statement.'';
        COMMENT ON COLUMN audit.%1$s.application_name IS ''Application name set when this audit event occurred. Can be changed in-session by client.'';
        COMMENT ON COLUMN audit.%1$s.action IS ''Action type; I = insert, D = delete, U = update, T = truncate'';
        COMMENT ON COLUMN audit.%1$s.row_data IS ''Record value. Null for statement-level trigger. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.'';
        COMMENT ON COLUMN audit.%1$s.changed_fields IS ''New values of fields changed by UPDATE. Null except for row-level UPDATE events.'';
        COMMENT ON COLUMN audit.%1$s.statement_only IS ''''''t'''' if audit event is from an FOR EACH STATEMENT trigger, ''''f'''' for FOR EACH ROW'';

        CREATE INDEX %1$s_relid_idx ON audit.%1$s(relid);
        CREATE INDEX %1$s_action_tstamp_tx_stm_idx ON audit.%1$s(action_tstamp_stm);
        CREATE INDEX %1$s_action_idx ON audit.%1$s(action);         
        ', table_names[i]);     

        EXECUTE format('
        DROP TRIGGER IF EXISTS audit_trigger_row ON %1$s;
        CREATE TRIGGER audit_trigger_row
        AFTER INSERT OR UPDATE OR DELETE
        ON public.%1$s
        FOR EACH ROW
        EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);

        EXECUTE format('
        DROP TRIGGER IF EXISTS audit_trigger_stm ON %1$s;
        CREATE TRIGGER audit_trigger_stm
        AFTER TRUNCATE
        ON public.%1$s
        FOR EACH STATEMENT
        EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);

    END LOOP;

RETURN 'SUCCESS';
END;
$BODY$
LANGUAGE plpgsql;
ALTER FUNCTION audit.create_audit_table(character varying[])
OWNER TO postgres;  

更新03/31:

好吧,所以我创建了没有动态sql的if_modified_func()函数,我将audit_row声明为audit_row RECORD;我不确定“在插入值时需要强制转换”的部分。我也不确定这是否是插入的正确方法

EXECUTE format($string$INSERT INTO audit.%1$s VALUES (audit_row.*);$string$, TG_TABLE_NAME::text);

当我运行select audit.create_audit_table(ARRAY['organization'])时,我现在收到此错误

错误:

ERROR:  record "audit_row" has no field "row_data"
CONTEXT:  PL/pgSQL function audit.if_modified_func() line 42 at assignment

这是更新的功能:

CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
        DECLARE     
            audit_row RECORD;            
            include_values boolean;
            log_diffs boolean;
            h_old hstore;
            h_new hstore;
            excluded_cols text[] = ARRAY[]::text[];
        BEGIN
            IF TG_WHEN <> 'AFTER' THEN
                RAISE EXCEPTION 'audit.if_modified_func() may only run as an AFTER trigger';
            END IF;

            audit_row = ROW(
                nextval(format('audit.%1$s_event_id_seq',TG_TABLE_NAME::text)), -- event_id
                TG_TABLE_SCHEMA::text,                        -- schema_name
                TG_TABLE_NAME::text,                          -- table_name
                TG_RELID,                                     -- relation OID for much quicker searches
                session_user::text,                           -- session_user_name
                current_timestamp,                            -- action_tstamp_tx
                statement_timestamp(),                        -- action_tstamp_stm
                clock_timestamp(),                            -- action_tstamp_clk
                txid_current(),                               -- transaction ID
                current_setting('application_name'),          -- client application
                inet_client_addr(),                           -- client_addr
                inet_client_port(),                           -- client_port
                current_query(),                              -- top-level query or queries (if multistatement) from client
                substring(TG_OP,1,1),                         -- action
                NULL, NULL,                                   -- row_data, changed_fields
                'f'                                           -- statement_only
                );

            IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
                audit_row.client_query = NULL;
            END IF;

            IF TG_ARGV[1] IS NOT NULL THEN
                excluded_cols = TG_ARGV[1]::text[];
            END IF;

            IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
                audit_row.row_data = hstore(OLD.*) - excluded_cols;
                audit_row.changed_fields =  (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
                IF audit_row.changed_fields = hstore('') THEN
                    -- All changed fields are ignored. Skip this update.
                    RETURN NULL;
                END IF;
            ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
                audit_row.row_data = hstore(OLD.*) - excluded_cols;
            ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
                audit_row.row_data = hstore(NEW.*) - excluded_cols;
            ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
                audit_row.statement_only = 't';
            ELSE
                RAISE EXCEPTION '[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
                RETURN NULL;
            END IF;
            EXECUTE format('INSERT INTO audit.%1$s VALUES (audit_row.*)', TG_TABLE_NAME::text);  

        RETURN null;
        END;
        $$
        LANGUAGE plpgsql;   
        ALTER FUNCTION audit.if_modified_func()
        OWNER TO postgres;
postgresql function triggers plpgsql
2个回答
0
投票

TG_TABLE_NAME是一个特殊的触发变量,仅在触发函数内可用。你的create_audit_table()不是触发器功能。

此外,您不断重新定义您的真实触发函数(if_modified_func()),它会使之前创建的任何触发器“无效”。

在没有动态SQL魔法的情况下创建触发器函数(动态SQL只需要向这些审计表插入值)。然后,您可以将审计逻辑添加到表中,其中包含:

CREATE TRIGGER audit_trigger_row
  AFTER INSERT OR UPDATE OR DELETE
  ON public.<your_table_name>
  FOR EACH ROW
  EXECUTE PROCEDURE <your_audit_trigger_function_name>();

你可以把这个(但只有这个 - 可能是drop if exists)放在一个函数中,以便更容易地附加这个审计逻辑。

笔记:

  • 在触发器功能中,你不能使用%ROWTYPE变量(因为你不知道确切的表。你只有它的名字)。解决方案很简单:只需使用RECORD类型(虽然插入值时需要使用强制转换)。
  • 不要对这么长的字符串使用单引号。请改用$your_keyword$<string_value>$your_keyword$格式。可能使用format()函数而不是仅仅连接值。您的代码将更具可读性。

编辑:要使用你的RECORD变量,你应该:

  • 用结构初始化它。您可以使用f.ex执行此操作。 SELECT nextval('audit.'|| quote_ident(TG_TABLE_NAME) || '_event_id_seq') AS event_id, TG_TABLE_SCHEMA AS schema_name, TG_TABLE_NAME AS table_name, TG_RELID AS relid, session_user AS session_user_name, current_timestamp AS action_tstamp_tx, statement_timestamp() AS action_tstamp_stm, clock_timestamp() AS action_tstamp_clk, txid_current() AS transaction_id, current_setting('application_name') AS application_name, inet_client_addr() AS client_addr, inet_client_port() AS client_port, current_query() AS client_query, substring(TG_OP, 1, 1) AS action, NULL::hstore AS row_data, NULL::hstore AS changed_fields, FALSE AS statement_only INTO audit_row;
  • 使用ROW()构造函数的预定义名称。第一列的名字是f1,第二列的是f2等。 audit_row.f15 = hstore(OLD.*) - excluded_cols;

选择上述方法之一后,您应该插入如下行:

EXECUTE format('INSERT INTO audit.%1$s VALUES (($1::text::audit.%1$s).*)', quote_ident(TG_TABLE_NAME)) USING audit_row;

注意:由于text无法知道EXECUTE的实际结构,因此甚至需要施放到audit_row

http://rextester.com/GUAJ1339


1
投票

quote_ident(TG_TABLE_NAME :: TEXT)将应用必要的操作来正确引用参数作为关系名称。

我建议使用execute format('statement')而不是concatinations,例如:

t=# do $$ begin raise info '%',format('I am %I, now is %L',current_user,now()); end;$$;
INFO:  I am postgres, now is '2017-03-30 07:33:53.579476+00'
DO

代替:

t=# do $$ begin raise info '%','I am '||quote_ident(current_user)||', now is '||quote_ident(now()::text); end;$$;
INFO:  I am postgres, now is "2017-03-30 07:36:20.495887+00"
DO
© www.soinside.com 2019 - 2024. All rights reserved.