# PostgreSQL JSON 與 Trigger 應用
## 前言
一般trigger 做 audit,需要有一個紀錄變動之前或變動之後的table,會與原table類似,但是需要更多欄位,新舊,action(insert不一定會有,看需要,update或delete),再補個timestamp.在以前的例子中可以看到.
但是當原table的schema變動時,就很麻煩!需要做對應的變動,trigger也要改寫,雖然沒太大的技術性,但是需要很小心.若是欄位很多時,這個trigger 寫起來繁瑣又無趣.
## 搭配JSON使用
有了JSON以後,會更方便.雖然以前PG就有hstore了,但是hstore與其他DB或程式語言的整合度不比JSON.
### 範例資料
```sql!=
create table it230117 (
id int generated always as identity primary key
, gname text
, gprice int
);
create table it230117_audit (
id int generated always as identity primary key
, action text
, tid int
, oldval jsonb
, newval jsonb
);
create or replace function trifun_it230117()
returns trigger
as $_$
begin
if (TG_OP = 'INSERT') then
insert into it230117_audit(action, tid, newval)
values (TG_OP, NEW.id, to_jsonb(NEW.*));
elsif (TG_OP = 'DELETE') then
insert into it230117_audit(action, tid, oldval)
values (TG_OP, OLD.id, to_jsonb(OLD.*));
elsif (TG_OP = 'UPDATE') then
insert into it230117_audit(action, tid, oldval, newval)
values (TG_OP, OLD.id, to_jsonb(OLD.*), to_jsonb(NEW.*));
end if;
return null;
end;
$_$ language plpgsql;
create trigger tri_it230117
after insert or delete or update
on it230117
for each row execute function trifun_it230117();
```
### 測試一下
```sql!=
insert into it230117 (gname, gprice) values
('鉛筆', 5), ('原子筆', 7), ('簽字筆', 8);
select *
from it230117;
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 1 | 鉛筆 | 5 |
| 2 | 原子筆 | 7 |
| 3 | 簽字筆 | 8 |
+----+--------+--------+
(3 rows)
select *
from it230117_audit;
+----+--------+-----+--------+-------------------------------------------+
| id | action | tid | oldval | newval |
+----+--------+-----+--------+-------------------------------------------+
| 1 | INSERT | 1 | [NULL] | {"id": 1, "gname": "鉛筆", "gprice": 5} |
| 2 | INSERT | 2 | [NULL] | {"id": 2, "gname": "原子筆", "gprice": 7} |
| 3 | INSERT | 3 | [NULL] | {"id": 3, "gname": "簽字筆", "gprice": 8} |
+----+--------+-----+--------+-------------------------------------------+
(3 rows)
update it230117
set gprice = gprice + 1;
select *
from it230117_audit
where id > 3;
+----+--------+-----+-------------------------------------------+-------------------------------------------+
| id | action | tid | oldval | newval |
+----+--------+-----+-------------------------------------------+-------------------------------------------+
| 4 | UPDATE | 1 | {"id": 1, "gname": "鉛筆", "gprice": 5} | {"id": 1, "gname": "鉛筆", "gprice": 6} |
| 5 | UPDATE | 2 | {"id": 2, "gname": "原子筆", "gprice": 7} | {"id": 2, "gname": "原子筆", "gprice": 8} |
| 6 | UPDATE | 3 | {"id": 3, "gname": "簽字筆", "gprice": 8} | {"id": 3, "gname": "簽字筆", "gprice": 9} |
+----+--------+-----+-------------------------------------------+-------------------------------------------+
(3 rows)
delete
from it230117
where id = 3;
select *
from it230117_audit
where id > 6;
+----+--------+-----+-------------------------------------------+--------+
| id | action | tid | oldval | newval |
+----+--------+-----+-------------------------------------------+--------+
| 7 | DELETE | 3 | {"id": 3, "gname": "簽字筆", "gprice": 9} | [NULL] |
+----+--------+-----+-------------------------------------------+--------+
(1 row)
```
## 由JSON還原為record
雖然已經有紀錄起來了,但是還原為record,讓SQL語法可以應用,會更方便.
在此可以使用 jsonb_populate_record()
### 直接呼叫
```sql!=
select jsonb_populate_record(null::it230117, oldval)
from it230117_audit
where action = 'UPDATE';
+-----------------------+
| jsonb_populate_record |
+-----------------------+
| (1,鉛筆,5) |
| (2,原子筆,7) |
| (3,簽字筆,8) |
+-----------------------+
```
雖然還原為record了,但是是整個row.取用不便.
### 要用 join lateral
```sql!=
select r.*
from it230117_audit
cross join lateral jsonb_populate_record(null::it230117, oldval) r
where action = 'UPDATE';
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 1 | 鉛筆 | 5 |
| 2 | 原子筆 | 7 |
| 3 | 簽字筆 | 8 |
+----+--------+--------+
(3 rows)
select r.*
, '<-old new->' as "指示牌"
, s.*
from it230117_audit
cross join lateral jsonb_populate_record(null::it230117, oldval) r
cross join lateral jsonb_populate_record(null::it230117, newval) s
where action = 'UPDATE';
+----+--------+--------+-------------+----+--------+--------+
| id | gname | gprice | 指示牌 | id | gname | gprice |
+----+--------+--------+-------------+----+--------+--------+
| 1 | 鉛筆 | 5 | <-old new-> | 1 | 鉛筆 | 6 |
| 2 | 原子筆 | 7 | <-old new-> | 2 | 原子筆 | 8 |
| 3 | 簽字筆 | 8 | <-old new-> | 3 | 簽字筆 | 9 |
+----+--------+--------+-------------+----+--------+--------+
(3 rows)
select r.*
from it230117_audit
cross join lateral jsonb_populate_record(null::it230117, oldval) r
where action = 'DELETE';
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 3 | 簽字筆 | 9 |
+----+--------+--------+
(1 row)
select r.*
from it230117_audit
cross join lateral jsonb_populate_record(null::it230117, newval) r
where action = 'INSERT';
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 1 | 鉛筆 | 5 |
| 2 | 原子筆 | 7 |
| 3 | 簽字筆 | 8 |
+----+--------+--------+
(3 rows)
-- 再來點 update
select * from it230117;
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 1 | 鉛筆 | 6 |
| 2 | 原子筆 | 8 |
+----+--------+--------+
(2 rows)
-- 現在資料剩兩筆
update it230117
set gname = '2B鉛筆'
, gprice = 7
where id = 1;
select r.*
, '<-old new->' as "指示牌"
, s.*
from it230117_audit
cross join lateral jsonb_populate_record(null::it230117, oldval) r
cross join lateral jsonb_populate_record(null::it230117, newval) s
where action = 'UPDATE'
and r.gname like '%鉛筆%';
+----+-------+--------+-------------+----+--------+--------+
| id | gname | gprice | 指示牌 | id | gname | gprice |
+----+-------+--------+-------------+----+--------+--------+
| 1 | 鉛筆 | 5 | <-old new-> | 1 | 鉛筆 | 6 |
| 1 | 鉛筆 | 6 | <-old new-> | 1 | 2B鉛筆 | 7 |
+----+-------+--------+-------------+----+--------+--------+
(2 rows)
```
## 還有沒有進一步的用法?
上面的例子中,把 insert, update, delete 做了記錄,也可以取用為record了.但是對table的操作中,還有一個 truncate !
### 改寫下trigger
```sql!=
create or replace function trif_trun_it230117()
returns trigger
as $_$
begin
insert into it230117_audit(action,oldval)
select 'TRUNCATE',jsonb_agg(to_jsonb(i.*))
from it230117 i;
return null;
end;
$_$ language plpgsql;
create trigger tri_trun_it230117
before truncate
on it230117
for each statement execute function trif_trun_it230117();
```
### truncate 與驗證
```sql!=
truncate table it230117
restart identity;
-- 檢查一下 it230117_audit
select oldval
from it230117_audit
where action = 'TRUNCATE';
+----------------------------------------------------------------------------------------+
| oldval |
+----------------------------------------------------------------------------------------+
| [{"id": 2, "gname": "原子筆", "gprice": 8}, {"id": 1, "gname": "2B鉛筆", "gprice": 7}] |
+----------------------------------------------------------------------------------------+
(1 row)
```
果然將資料存起來了,注意到存起來的是 JSONB ARRAY.
### 還原
多筆資料聚合,存成JSONB ARRAY,所以還原的方式,是呼叫不同的函數.
jsonb_populate_recordset()
```sql!=
select r.*
from it230117_audit
cross join lateral jsonb_populate_recordset(null::it230117, oldval) r
where action = 'TRUNCATE';
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 2 | 原子筆 | 8 |
| 1 | 2B鉛筆 | 7 |
+----+--------+--------+
(2 rows)
-- 當然可以排序
select r.*
, pg_typeof(r.id)
from it230117_audit
cross join lateral jsonb_populate_recordset(null::it230117, oldval) r
where action = 'TRUNCATE'
order by r.id;
+----+--------+--------+-----------+
| id | gname | gprice | pg_typeof |
+----+--------+--------+-----------+
| 1 | 2B鉛筆 | 7 | integer |
| 2 | 原子筆 | 8 | integer |
+----+--------+--------+-----------+
(2 rows)
```
### PostgreSQL 是怎樣知道對應的資料型態的?
可以觀察到,還原的資料,不是還原成JSON(B) 一些取用的運算子或是函數的text型態,我們後續使用還需要做轉型,而是直接與table對應的.
因為PostgreSQL在我們建立table時,會同時建立對應的type.
這樣我們就可以應用了!
在官網的文件中
https://www.postgresql.org/docs/current/functions-json.html
是另外建立了type, 透過以上的例子與官網資料,可以靈活的應用,除了還原為與原table相同的,也可以另外定義type,將資料轉為我們想要的型態,當然是要能夠正確轉換的.
附帶提一下,在前面truncate時,我加上了 restart identity,可以讓對應的identity 欄位重新計數,就不需要再去找table column 對應的 sequence 來做reset了.
### 復活吧 安蘇納姆
```sql!=
insert into it230117(gname, gprice)
select r.gname, r.gprice
from it230117_audit
cross join lateral jsonb_populate_recordset(null::it230117, oldval) r
where action = 'TRUNCATE'
order by r.id;
select *
from it230117;
+----+--------+--------+
| id | gname | gprice |
+----+--------+--------+
| 1 | 2B鉛筆 | 7 |
| 2 | 原子筆 | 8 |
+----+--------+--------+
(2 rows)
```
## 結語
PostgreSQL提供了許多強大的功能,與其他DB都在持續進步當中.
資訊科技進步日新月異,希望這個小小的心得分享,能夠帶給大家一些參考資訊.