DRY postgresql 9.4触发函数

时间:2021-02-13 22:54:21

I've written 3 functions to log transactions to designated tables:

我写了3个函数来将事务记录到指定的表:

CREATE OR REPLACE FUNCTION log_sites() RETURNS TRIGGER AS $body$
DECLARE
    target_row sites%ROWTYPE;
BEGIN
    IF (TG_OP = 'DELETE') THEN
        -- No NEW row
        target_row = OLD;
    ELSE
        target_row = NEW;
    END IF;
    INSERT INTO sites_history (transaction_type,
                               transaction_time,
                               site_id,
                               address,
                               name,
                               shared_key)
    VALUES (TG_OP,
            NOW(),
            target_row.site_id,
            target_row.address,
            target_row.name,
            target_row.shared_key);
RETURN target_row;
END;
$body$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION log_licenses() RETURNS TRIGGER AS $body$
DECLARE
    target_row licenses%ROWTYPE;
BEGIN
    IF (TG_OP = 'DELETE') THEN
        target_row = OLD;
    ELSE
        target_row = NEW;
    END IF;
    INSERT INTO licenses_history (transaction_type,
                                  transaction_time,
                                  license_id,
                                  start_date,
                                  expiration_date,
                                  site_id)
    VALUES (TG_OP,
            NOW(),
            target_row.license_id,
            target_row.start_date,
            target_row.expiration_date,
            target_row.site_id);
    RETURN target_row;
    END;
    $body$
    LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION log_clients() RETURNS TRIGGER AS $body$
DECLARE
    target_row clients%ROWTYPE;
BEGIN
    IF (TG_OP = 'DELETE') THEN
        target_row = OLD;
    ELSE
        target_row = NEW;
    END IF;
    INSERT INTO clients_history (transaction_type,
                                 transaction_time,
                                 mac_address,
                                 hardware,
                                 license_id,
                                 site_id)
    VALUES (TG_OP,
            NOW(),
            target_row.mac_address,
            target_row.hardware,
            target_row.license_id,
            target_row.site_id);
    RETURN target_row;
    END;
    $body$
    LANGUAGE plpgsql;

This results in a big ugly block of PL/pgSQL, which nobody at my job is especially familiar with, myself included. A coworker suggested it'd be nice to consolidate/DRYify all this stuff, but for the life of me, I'm not sure how, especially considering each table needs a separate trigger, and the way triggers pass data to their functions. Any suggestions?

这导致了一个巨大的丑陋的PL / pgSQL块,我的工作中没有人特别熟悉,包括我自己。一位同事建议合并/ DRYify所有这些东西都很好,但对于我的生活,我不确定如何,特别是考虑到每个表需要一个单独的触发器,并且触发方式将数据传递给他们的函数。有什么建议?

ETA: 1) Here are the triggers:

ETA:1)以下是触发器:

CREATE TRIGGER sites_log
AFTER INSERT OR UPDATE OR DELETE
ON sites
FOR EACH ROW EXECUTE PROCEDURE log_transactions();

CREATE TRIGGER licenses_log
    AFTER INSERT OR UPDATE OR DELETE
    ON licenses
    FOR EACH ROW EXECUTE PROCEDURE log_transactions();

CREATE TRIGGER clients_log
    AFTER INSERT OR UPDATE OR DELETE
    ON clients
    FOR EACH ROW EXECUTE PROCEDURE log_transactions();

Here's what I've got now, after quite a bit of messing around:

这是我现在得到的东西,经过相当多的搞乱:

CREATE OR REPLACE FUNCTION log_transactions() RETURNS TRIGGER LANGUAGE plpgsql AS $body$
DECLARE
    target_row RECORD;
    target_cols text[];
    col_name RECORD;
    col_name_str text;
    right_now timestamp without time zone;
    q_str text;
BEGIN
    right_now := now();
    target_cols := '{}';
    FOR col_name IN SELECT column_name::text FROM information_schema.columns WHERE table_name = TG_TABLE_NAME AND table_schema = TG_TABLE_SCHEMA LOOP
        col_name_str := col_name.column_name::text;
        target_cols = ARRAY_APPEND(target_cols, col_name_str);
    END LOOP;
    RAISE NOTICE 'target_cols: %', target_cols;
    IF (TG_OP = 'DELETE') THEN
        target_row := OLD;
    ELSE
        target_row := NEW;
    END IF; 

    RAISE NOTICE 'target_row: %', target_row;
    EXECUTE format('INSERT INTO %I_history (transaction_time, transaction_type) VALUES (%L, %L)', TG_TABLE_NAME, right_now, TG_OP);
    q_str := format('UPDATE %I_history SET (%s) = ', TG_TABLE_NAME, array_to_string(target_cols, ', ')) || '$1' || format(' WHERE transaction_type = %L AND transaction_time = %L', TG_OP, right_now);
    EXECUTE q_str USING target_row;
    RETURN target_row;
END;
$body$;

This doesn't work either, and it's spiraling out of control, complexity-wise.

这也不起作用,并且它正在逐渐失控,复杂性。

1 个解决方案

#1


Personally, I use a set of home-grown functions Below for auditing any table I want. All I do is run audit.enable() on any table I want to audit and it's stored in the general tables I have here. Yes, it's not exactly what you're doing, but it's the most "DRY" think there is really, I do it once and never again-- ever.

就个人而言,我使用一组自行开发的函数来审核我想要的任何表。我所做的只是在我想要审计的任何表上运行audit.enable(),它存储在我这里的常规表中。是的,这不完全是你正在做的事情,但它是最“干”的认为真的,我曾经做过一次而且再也没有 - 永远。

CREATE TABLE audit.audit_log 
(
    audit_log_seq serial primary key,
    schema_name text NOT NULL,
    table_name text NOT NULL,
    db_username text,
    user_seq bigint,
    logged_ip inet,
    log_timestamp TIMESTAMP NOT NULL DEFAULT (now()),
    action TEXT NOT NULL CHECK (action IN ('I','D','U')),
    comment varchar(500),
    old_data hstore,
    new_data hstore,
    query text
) WITH 
(
    fillfactor=100
);

CREATE INDEX audit_log_schema_table_idx ON audit.audit_log(schema_name,table_name);
CREATE INDEX audit_log_timestamp_utc_idx ON audit.audit_log(log_timestamp);
CREATE INDEX audit_log_uname on audit.audit_log(user_seq);



-- generic function for all tables


CREATE OR REPLACE FUNCTION audit.log_func() RETURNS TRIGGER AS $body$
DECLARE
    v_old_data hstore;
    v_new_data hstore;  
    v_query text;
    v_comment varchar;
--  v_old_data TEXT;
--  v_new_data TEXT;
BEGIN
    v_query=current_query();

    IF (TG_OP = 'UPDATE') THEN
        v_old_data := hstore(OLD.*);
        v_new_data := hstore(NEW.*);
        v_comment=v_new_data -> 'audit_comment';
    ELSIF (TG_OP = 'DELETE') THEN
        v_old_data := hstore(OLD.*);
    ELSIF (TG_OP = 'INSERT') THEN
        v_new_data := hstore(NEW.*);
    v_comment=v_new_data -> 'audit_comment';
    ELSE
        RAISE WARNING '[audit.log_func] - Other action occurred: %, at %',TG_OP,now();
        RETURN NULL;
    END IF;


    INSERT INTO audit.audit_log (schema_name,table_name,db_username,user_seq,logged_ip,action,old_data,new_data,query, comment) 
            VALUES (TG_TABLE_SCHEMA::TEXT,
            TG_TABLE_NAME::TEXT,
            session_user::TEXT,
            coalesce(current_setting('mvc.user_seq'),'0')::bigint, --current user
            inet_client_addr(),
            substring(TG_OP,1,1),
            v_old_data,
            v_new_data,
            v_query,
           v_comment);

    IF (TG_OP = 'DELETE') THEN
        RETURN OLD;
    END IF;
    RETURN NEW;

EXCEPTION
    WHEN data_exception THEN
        RAISE WARNING '[audit.log_func] - UDF ERROR [DATA EXCEPTION] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
        RETURN NULL;
    WHEN unique_violation THEN
        RAISE WARNING '[audit.log_func] - UDF ERROR [UNIQUE] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
        RETURN NULL;
    --WHEN OTHERS THEN
    --    RAISE WARNING '[audit.log_func] - UDF ERROR [OTHER] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
    --    RETURN NULL;
END;
$body$ language plpgsql security definer;

CREATE OR REPLACE FUNCTION audit.enable(p_table_name text,p_schema_name text DEFAULT 'dallas') RETURNS VOID as $body$
DECLARE
BEGIN
EXECUTE 'create trigger trg_audit_'||p_table_name||' BEFORE INSERT OR UPDATE OR DELETE ON '||p_schema_name||'.'||p_table_name|| ' FOR EACH ROW EXECUTE PROCEDURE audit.log_func()'; 
exception when duplicate_object then null;
END;
$body$ language plpgsql security definer;

#1


Personally, I use a set of home-grown functions Below for auditing any table I want. All I do is run audit.enable() on any table I want to audit and it's stored in the general tables I have here. Yes, it's not exactly what you're doing, but it's the most "DRY" think there is really, I do it once and never again-- ever.

就个人而言,我使用一组自行开发的函数来审核我想要的任何表。我所做的只是在我想要审计的任何表上运行audit.enable(),它存储在我这里的常规表中。是的,这不完全是你正在做的事情,但它是最“干”的认为真的,我曾经做过一次而且再也没有 - 永远。

CREATE TABLE audit.audit_log 
(
    audit_log_seq serial primary key,
    schema_name text NOT NULL,
    table_name text NOT NULL,
    db_username text,
    user_seq bigint,
    logged_ip inet,
    log_timestamp TIMESTAMP NOT NULL DEFAULT (now()),
    action TEXT NOT NULL CHECK (action IN ('I','D','U')),
    comment varchar(500),
    old_data hstore,
    new_data hstore,
    query text
) WITH 
(
    fillfactor=100
);

CREATE INDEX audit_log_schema_table_idx ON audit.audit_log(schema_name,table_name);
CREATE INDEX audit_log_timestamp_utc_idx ON audit.audit_log(log_timestamp);
CREATE INDEX audit_log_uname on audit.audit_log(user_seq);



-- generic function for all tables


CREATE OR REPLACE FUNCTION audit.log_func() RETURNS TRIGGER AS $body$
DECLARE
    v_old_data hstore;
    v_new_data hstore;  
    v_query text;
    v_comment varchar;
--  v_old_data TEXT;
--  v_new_data TEXT;
BEGIN
    v_query=current_query();

    IF (TG_OP = 'UPDATE') THEN
        v_old_data := hstore(OLD.*);
        v_new_data := hstore(NEW.*);
        v_comment=v_new_data -> 'audit_comment';
    ELSIF (TG_OP = 'DELETE') THEN
        v_old_data := hstore(OLD.*);
    ELSIF (TG_OP = 'INSERT') THEN
        v_new_data := hstore(NEW.*);
    v_comment=v_new_data -> 'audit_comment';
    ELSE
        RAISE WARNING '[audit.log_func] - Other action occurred: %, at %',TG_OP,now();
        RETURN NULL;
    END IF;


    INSERT INTO audit.audit_log (schema_name,table_name,db_username,user_seq,logged_ip,action,old_data,new_data,query, comment) 
            VALUES (TG_TABLE_SCHEMA::TEXT,
            TG_TABLE_NAME::TEXT,
            session_user::TEXT,
            coalesce(current_setting('mvc.user_seq'),'0')::bigint, --current user
            inet_client_addr(),
            substring(TG_OP,1,1),
            v_old_data,
            v_new_data,
            v_query,
           v_comment);

    IF (TG_OP = 'DELETE') THEN
        RETURN OLD;
    END IF;
    RETURN NEW;

EXCEPTION
    WHEN data_exception THEN
        RAISE WARNING '[audit.log_func] - UDF ERROR [DATA EXCEPTION] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
        RETURN NULL;
    WHEN unique_violation THEN
        RAISE WARNING '[audit.log_func] - UDF ERROR [UNIQUE] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
        RETURN NULL;
    --WHEN OTHERS THEN
    --    RAISE WARNING '[audit.log_func] - UDF ERROR [OTHER] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
    --    RETURN NULL;
END;
$body$ language plpgsql security definer;

CREATE OR REPLACE FUNCTION audit.enable(p_table_name text,p_schema_name text DEFAULT 'dallas') RETURNS VOID as $body$
DECLARE
BEGIN
EXECUTE 'create trigger trg_audit_'||p_table_name||' BEFORE INSERT OR UPDATE OR DELETE ON '||p_schema_name||'.'||p_table_name|| ' FOR EACH ROW EXECUTE PROCEDURE audit.log_func()'; 
exception when duplicate_object then null;
END;
$body$ language plpgsql security definer;