先直接來看例子
create table gals1 (
id int generated always as identity primary key
, name text not null unique
);
insert into gals1 (name)
values ('小島南')
returning *;
id | name
----+--------
1 | 小島南
(1 row)
INSERT 0 1
--
insert into gals1 (name)
values ('佐山愛')
returning *;
id | name
----+--------
2 | 佐山愛
(1 row)
INSERT 0 1
---
再次 insert 小島南
insert into gals1 (name)
values ('小島南')
returning *;
ERROR: 23505: duplicate key value violates unique constraint "gals1_name_key"
DETAIL: Key (name)=(小島南) already exists.
SCHEMA NAME: elixir
TABLE NAME: gals1
CONSTRAINT NAME: gals1_name_key
這個操作(單純insert)就不是冪等(idempotent).
因為要回應相同的結果.
接著先來看一個時常見到的需求.
就是要做insert,但是若有重複,就不要insert.
所以要先做查詢,判斷是否有找到.
很多人會使用一般自己熟悉的程式語言,來寫個function,
使用 if else 把兩道SQL組合起來.
這裡先用 plpgsql 來寫個function.
create or replace function select_insert_gals1 (_name text)
returns setof gals1
language plpgsql as
$code$
declare
rtn gals1%rowtype;
begin
select * into rtn
from gals1
where name = _name;
if found then
return next rtn;
else
return query
insert into gals1 (name)
values (_name)
returning *;
end if;
end;
$code$;
---
select select_insert_gals1('河北彩伽');
select_insert_gals1
---------------------
(4,河北彩伽)
(1 row)
備註: 3 因為剛才 第二次 insert 小島南時會先產生,
然後 unique 生效, 回報錯誤.此時。sequence 是 3.
再來一次 河北彩伽
select select_insert_gals1('河北彩伽');
select_insert_gals1
---------------------
(4,河北彩伽)
驗證一下 佐山愛
select select_insert_gals1('佐山愛');
select_insert_gals1
---------------------
(2,佐山愛)
驗證一下 小島南
select *
from select_insert_gals1('小島南');
id | name
----+--------
1 | 小島南
因為此函數回傳的結果是如同 gals1,是 set returning function.
所以使用 select * from func()這樣的格式.
直接 select func() 這樣的格式, Postgresql 會把結果
使用 row 方式展現.
也可以把 row 轉成 json 格式.
select row_to_json(n.*)
from select_insert_gals1('河北彩伽') as n;
row_to_json
----------------------------
{"id":4,"name":"河北彩伽"}
(1 row)
由上面例子可以看到,使用SQL 搭配函數,十分地靈活好用.
還可以這樣用.
select current_gals.*
from (values ('夢乃あいか'), ('石川澪'), ('佐山愛') ) as newgals(name)
, select_insert_gals1(name) as current_gals;
id | name
----+------------
5 | 夢乃あいか
6 | 石川澪
2 | 佐山愛
(3 rows)
由上面的結果,可以觀察到, 夢乃あいか, 石川澪 是新增,佐山愛是原本有的.
而 newgals 可以是另一個table.
這樣直接使用一道SQL,就完成了!
而不是寫一隻程式,逐筆讀取新值, 然後先做查詢(select),判斷,再insert.
若沒搭配使用 retuning 時, 像MySQL 會有一個 last_insert_id() ,
有的會使用此函數取得PK,然後再次查詢.如此一來,透過迴圈,並多次讀寫,
效率自然不是很好.甚至有的並未使用(不知道)last_insert_id(),就會由外部程式
來產生uuid,並堅持一定只能這樣.
其實若有觀察到,我們上面的例子中,並未使用PK來判斷.而是使用 unique constraint.
雖然已經有達到冪等操作(請看上面的佐山愛),但是還有另一個坎,就是 Race condition.
先來查看文件
按我前往
裡面提到也要參考 The condition names can be any of those shown in Appendix A.
其中有
23505 unique_violation
接著我們來演化一下 function, 建立一個新的好了.
create or replace function insert_gals1 (_name gals1.name%type)
returns setof gals1
language plpgsql as
$code$
declare
rtn gals1%rowtype;
begin
select * into rtn
from gals1
where name = _name;
if found then
return next rtn;
else
begin
return query
insert into gals1 (name)
values (_name)
returning *;
exception when unique_violation then
return query
select *
from gals1
where name = _name;
end;
end if;
end;
$code$;
這個新的函數, 利用 exception 來處理 unique_violation,
這樣當有競爭發生時,落後的 process, 就是改做查詢,符合了冪等操作.
另外注意到輸入參數,改為使用 %type 方式,會更加靈活.
有發現到這個函數一開始的查詢部分,跟後面excption 的處理部分,幾乎相同.
換一個說法就是,因為有了 unique constraint, 而函數又能改處理 unique_violation,
所以此函數可以改為 先 insert 吧, 發生 unique_violation 時,就查詢.
create or replace function insert_gals1 (_name gals1.name%type)
returns setof gals1
language plpgsql as
$code$
begin
return query
insert into gals1 (name)
values (_name)
returning *;
exception when unique_violation then
return query
select *
from gals1
where name = _name;
end;
$code$;
就是把原本 else 之後的 block 拿來就可以了.還減少了 declare 部分.
驗證看看
select current_gals.*
from (values ('夢乃あいか'), ('本鄉愛'), ('明里つむぎ') ) as newgals(name)
, insert_gals1(name) as current_gals;
id | name
----+------------
5 | 夢乃あいか
8 | 本鄉愛
9 | 明里つむぎ
(3 rows)
夢乃あいか 一樣是 id 5.
-------------
現在來看一下 gals1 裡的內容
id | name
----+------------
1 | 小島南
2 | 佐山愛
4 | 河北彩伽
5 | 夢乃あいか
6 | 石川澪
8 | 本鄉愛
9 | 明里つむぎ
(7 rows)
3 , 第二次 insert 小島南.
6 , 第二次 insert 夢乃あいか.
消耗掉
https://www.postgresql.org/docs/current/sql-insert.html
[ ON CONFLICT [ conflict_target ] conflict_action ]
where conflict_target can be one of:
( { index_column_name | ( index_expression ) } [ COLLATE collation ] [ opclass ] [, ...] ) [ WHERE index_predicate ]
ON CONSTRAINT constraint_name
and conflict_action is one of:
DO NOTHING
DO UPDATE SET { column_name = { expression | DEFAULT } |
( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) |
( column_name [, ...] ) = ( sub-SELECT )
} [, ...]
[ WHERE condition ]
使用 do nothing
建立新的 table, 與 gals1 相同
create table gals3
(like gals1 including all);
\d gals3
Table "elixir.gals3"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+------------------------------
id | integer | | not null | generated always as identity
name | text | | not null |
Indexes:
"gals3_pkey" PRIMARY KEY, btree (id)
"gals3_name_key" UNIQUE CONSTRAINT, btree (name)
constraint, index 都如同 gals1.
當然是沒有資料的.
select *
from gals3;
id | name
----+------
(0 rows)
--
接著使用 insert on conflict do nothing 的方式
insert into gals3 (name)
values ('小島南'), ('佐山愛')
on conflict (name)
do nothing
returning *;
id | name
----+--------
1 | 小島南
2 | 佐山愛
(2 rows)
-- 再來一次
id | name
----+------
(0 rows)
INSERT 0 0
可以觀察到,雖然不會重複輸入,但是 returning 是沒有資料的.
搭配 cte 與 union all
with t1 as (
insert into gals3 (name)
values ('河北彩伽'), ('小島南'), ('佐山愛')
on conflict (name)
do nothing
returning *
)
select *
from t1
union all
select *
from gals3
where name in ('河北彩伽', '小島南', '佐山愛');
id | name
----+----------
5 | 河北彩伽
1 | 小島南
2 | 佐山愛
(3 rows)
id 3, 4 在第二次 insert 時消耗了.
小島南, 佐山愛 的 id 保持 1, 2.
以上是多筆資料一次輸入的情境,無需另外開發函數,只需要組合成一道SQL即可.
可以參考呂健誠這篇
按我前往
感謝佐山愛,小島南,河北彩伽諸位.