Bootstrap

复杂的pgsql触发器实现数据审计:记录行变更值

一个触发器让它变得可更新,并且确保表中一行的任何插入、更新或删除被记录(即审计),本文记录在写触发器时候需要了解的一些语法知识点和实际应用

1、old 和new

数据库中触发器old和new的区别?

1.当使用insert语句的时候,如果原表中没有数据的话,那么对于插入数据后表来说新插入的那条数据就是new,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存,源站可能有防盗链机制,建议将图片保存下来直接上传失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nmrjxegffp-1688558759323)(3.png)]

2.当使用delete语句的时候,删除的那一条数据相对于删除数据后表的数据来说就是old,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwtCWEIC-1688558759324)(2.png)]

3.当使用update语句的时候,当修改原表数据的时候相对于修改数据后表的数据来说原表中修改的那条数据就是old,而修改数据后表被修改的那条数据就是new,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzBhm7t3-1688558759324)(1.png)]

获取新旧表中的字段的值,采用OLD.column和NEW.column进行表示。

  • NEW:数据类型是RECORD; 该变量为行级触发器中的INSERT/UPDATE操作存储新数据行。 在语句级别的触发器里这个变量以及DELETE操作未赋值。

  • OLD:数据类型是RECORD;该变量为行级触发器中的UPDATE/DELETE操作存储旧数据行。 在语句级别的触发器里以及对INSERT动作,这个变量未赋值。

  • TG_OP:数据类型是text;是一个说明激活触发器的操作的字符串 (INSERT, UPDATE,DELETE或者TRUNCATE)

2、json、jsonb

(1)pg查询结果一整行转换为JSON数组:

select array_to_json(array_agg(row_to_json(t))) 
from (SELECT * FROM test) t

在这里插入图片描述

(2)创建jsonb类型的值

jsonb _ build _ object

根据指定的“名称”和“值”创建 JSONB 对象。

语法格式

  <jsonb_build_object函数> ::= jsonb_build_object(<exp1>, <exp2> {, <exp3>, <exp4>})
  • 参数

  • <exp1>:指定“名称”,数据类型为 VARCHAR。
    
  • <exp2>:指定“值”,数据类型可以为任意类型。
    
  • 返回值

  • JSONB 对象。
    
  • 使用说明

  •  支持指定多个“名称”和“值”,jsonb_build_object 参数的个数必须为偶数。
    

sql语句

jsonb_build_object(key1,value1,key2,value2 ... keyN,valueN)

在这里插入图片描述

(3) jsonb _ build_array:转换为json数组

jsonb_build_array(value1,value2 ...)

在这里插入图片描述

参考链接:【Postgresql】jsonb类型创建、更新、删除、查询

(4)jsonb_agg
jsonb_agg 为集函数,将指定数据聚合成一个 JSONB 数组。

  • 语法格式

  • <jsonb_agg函数> ::= jsonb_agg(<exp>)
    
  • 参数

  • <exp>:指定数据,数据类型可以为任意类型。
    
  • 返回值

  • JSONB 数组。
    
  • 使用说明

  • 支持在<exp> 参数前指定 DISTINCT 关键字,即 jsonb_agg(DISTINCT <exp>),表示对<exp> 进行去重操作。
    

参考代码:实现查询结果导出Json

3、if语句

pgSQL中有两种条件语句分别为if与case语句。if 语句形式包含以下几种:

  • IF … THEN … END IF
  • IF … THEN … ELSE … END IF
  • IF … THEN … ELSIF … THEN … ELSE … END IF

示例:

create or replace function test_if(i int) returns void as $$
DECLARE
BEGIN
-- 替换对应if语句
END;
$$ LANGUAGE plpgsql;


(1)IFTHENEND IF
//该示例当输入值i大于10时会打印 i的值为:%
if i > 10 then
raise notice 'i的值为:%', i;
end if;

(2)IFTHENELSEEND IF
if i > 10 then
raise notice 'i的值大于10';
else
raise notice 'i的值小于等于10';
end if;

(3)IFTHEN … ELSIF … THENELSEEND IF
if i > 10 then
raise notice 'i的值大于10';
elsif i = 5 then
raise notice 'i的值为5';
else
raise notice 'i的值小于等于10';
end if;

IF代码参考链接

4、insert into select

INSERT INTO SELECT 语句从一个表复制数据,然后把数据插入到一个已存在的表中。其基本的书写格式为:

INSERT INTO table2 (c1,c2,c3...SELECT c1,c2,c3... 
FROM table1 
where ...

注意:

(1)要求目标表Table2和源表Table1必须存在,并且字段相应也必须存在

(2)注意Table2的主键约束,如果Table2有主键而且不为空,则 field1, field2…中必须包括主键

(3)注意语法,不要加values,和插入一条数据的sql混了

insert select 代码参考链接

5、union和union all

UNION 操作符用于合并两个或多个 SELECT 语句的结果集。

请注意:

UNION 内部的每个 SELECT 语句必须拥有相同数量的列。
另外,每个 SELECT 语句中的列也必须拥有相似的数据类型。
同时,每个 SELECT 语句中的列的顺序必须相同。

应用:

/*UNIONS 基础语法如下:*/
/*这里的条件语句可以根据您的需要设置任何表达式。*/
 
SELECT column1 [, column2 ]
FROM table1 [, table2 ]
[WHERE condition]
 
UNION
 
SELECT column1 [, column2 ]
FROM table1 [, table2 ]
[WHERE condition]

UNION ALL 操作符可以连接两个有重复行的 SELECT 语句
默认地,UNION 操作符选取不同的值。
如果允许重复的值,请使用 UNION ALL。

应用:

/*UINON ALL 子句基础语法如下:*/
/*这里的条件语句可以根据您的需要设置任何表达式。*/
 
SELECT column1 [, column2 ]
FROM table1 [, table2 ]
[WHERE condition]
 
UNION ALL
 
SELECT column1 [, column2 ]
FROM table1 [, table2 ]
[WHERE condition]

union 代码参考链接

6、loop循环

LOOP定义一个无条件的循环,它会无限重复直到被EXIT或RETURN语句终止。可选的label可以被EXIT和CONTINUE语句用在嵌套循环中指定这些语句引用的是哪一层循环。

[ <> ] LOOP
statements
END LOOP [ label ];

该示例会打印输出i的值,其中当i的值为5时,不会打印。

LOOP
i = i + 1;
EXIT WHEN i > 10;
CONTINUE WHEN i = 5;
raise notice 'i的值为:%',i;
END LOOP;

示例:LOOP和FOREACH 走数组循环。
在这里插入图片描述

LOOP代码参考链接

7、使用触发器记录表格变更前后的完整记录

应用:
postgresql 使用触发器来记录:谁改变了数据、数据什么使用被改、什么操作改变了数据、被改变前后的数据分别是什么。数据源表为T_S_DOORPLATE;审计表为T_S_DOORPLATE_LOG

审计表的字段结构
在这里插入图片描述
id:表格自增序列
uid:地址唯一标识码
old_value:变更前的地址记录
new_value:变更后的地址记录
change_time:变更时间
change_action:变更操作(INSERT、UPDATE、DELETE)

触发器函数:

CREATE OR REPLACE FUNCTION "public"."doorplate_recorder_trigger"()
  RETURNS "pg_catalog"."trigger" AS $BODY$
DECLARE
	old_row json := NULL;
	new_row json := NULL;
  uid VARCHAR;
BEGIN
  IF TG_OP IN ('UPDATE','DELETE') THEN
	old_row = row_to_json(OLD);
	uid = OLD.uid;
	END IF;
  IF TG_OP IN ('INSERT','UPDATE') THEN
  new_row = row_to_json(NEW);
	uid = NEW.uid;
  END IF;
	
	INSERT into public."T_S_DOORPLATE_LOG"(uid,old_value,new_value,change_action)
  VALUES (uid,old_row,new_row,TG_OP);
  RETURN NEW;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100

创建触发器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVwiM5zj-1688558759324)(4.png)]

触发器函数代码参考链接

8、pgsql函数:postgresql 比较OLD和NEW的所有值,并仅将更改的值存储在审计表中

该函数仅仅记录update变更前后的差异值,难点在于需要监控每个字段发生变更的情况,并且将变更情况存为json,由于可能存在一个提交同时改变多个字段的情况,因此需要用jsonArray进行储存。

目标json结构:

{
 "field_name": columnName, 
  "old_value" : OLD.COLUMN,
  "new_value" : NEW_COLUNMN 
  }

审计表的字段结构
在这里插入图片描述
数据源表为T_S_DOORPLATE;审计表为T_S_DOORPLATE_HISTORY

触发器函数代码:

CREATE OR REPLACE FUNCTION "public"."doorplate_history_trigger"()
  RETURNS "pg_catalog"."trigger" AS $BODY$
DECLARE
  col text ;
  txt text ;
BEGIN
  txt := 'SELECT CASE WHEN OLD.road != NEW.road THEN jsonb_build_object(''field_name'', road, ''old_value'', OLD.road, ''new_value'', NEW.road) ELSE NULL END AS changes_jsonb';
  FOREACH col IN ARRAY (ARRAY['road_direction','estate','building','unit', 'doorplate', 'doorplate_sub', 'room', 'house_license', 'license_time', 'category', 'owner', 'owner_phone', 'construction', 'principal', 'principal_phone', 'collect_time', 'linear_toponym', 'point_toponym', 'geometry', 'remarks'])
		LOOP 
		IF txt <> '' 
    THEN txt = txt || ' UNION ALL ' ;
    END IF ;
    txt := txt || 'SELECT CASE WHEN OLD.' || col || ' != NEW.' || col || E' THEN jsonb_build_object(''field_name'', ' || col || E', ''old_value'', OLD.' || col || E', ''new_value'', NEW.' || col || ') ELSE NOT END';
		END LOOP;
	EXECUTE format('INSERT INTO "T_S_DOORPLATE_HISTORY"(uid, change_jsonb)
		SELECT %I, jsonb_agg(l.changes_jsonb) FILTER (WHERE l.changes_jsonb IS NOT NULL) FROM ( '|| txt ||' ) AS l', OLD.uid);
	RETURN NEW;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100

注意点:(1)字符串拼接使用:=
(2)sql中单引号中的单引号用两个单引号表示'',双引号直接表示。
(3)UNION ALL 合并多个select结果
(4)RAISE NOTICE 'txt:%' , txt; 可以实现控制台输出
(5)FOREACH 关键词中间不能断开
(6)拼接字符串需要给个初始值''
(7)EXECUTE format 注意格式!(这里还有点bug,但总体代码结构是对的)

执行动态命令-pgsql官方文档链接

或者字段少的就可以不走循环,避免EXECUTE错误:

CREATE OR REPLACE FUNCTION "public"."doorplate_history_trigger"()
  RETURNS "pg_catalog"."trigger" AS $BODY$
DECLARE
  col text ;
  txt text ;
BEGIN
  INSERT INTO "T_S_DOORPLATE_HISTORY"(uid, change_jsonb)
		SELECT OLD.uid, jsonb_agg(l.changes_jsonb) FILTER (WHERE l.changes_jsonb IS NOT NULL) FROM (
				SELECT CASE WHEN OLD.road !=NEW.road THEN jsonb_build_object ('field_name','road','old_value',OLD.road,'new_value',NEW.road) ELSE NULL END AS changes_jsonb UNION ALL 
				SELECT CASE WHEN OLD.road_direction !=NEW.road_direction THEN jsonb_build_object ('field_name','road_direction','old_value',OLD.road_direction,'new_value',NEW.road_direction) ELSE NULL END UNION ALL 
				SELECT CASE WHEN OLD.estate !=NEW.estate THEN jsonb_build_object ('field_name','estate','old_value',OLD.estate,'new_value',NEW.estate) ELSE NULL END UNION ALL 
				SELECT CASE WHEN OLD.point_toponym !=NEW.point_toponym THEN jsonb_build_object ('field_name','point_toponym','old_value',OLD.point_toponym,'new_value',NEW.point_toponym) ELSE NULL END UNION ALL 
				SELECT CASE WHEN OLD.remarks !=NEW.remarks THEN jsonb_build_object ('field_name','remarks','old_value',OLD.remarks,'new_value',NEW.remarks) ELSE NULL END    
		 ) AS l;
	RETURN NEW;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100

触发器设置:
在这里插入图片描述

触发器代码参考链接

;