iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

2
Software Development

以Postgresql為主,聊聊資料庫.系列 第 31

Postgresql 使用 fdw 連接MySQL 將階層資料移轉並使用 ltree 產生路徑實例

Postgresql 使用 fdw 連接MySQL 將階層資料移轉並使用 ltree 產生路徑實例

昨天回答了邦友關於 MySQL 5.7 處理階層式資料.雖然開發了兩個 procedure,
但是畢竟不如使用有 CTE 及 Path Enumerations (ltree) 的 Postgresql
來的方便.

https://ithelp.ithome.com.tw/questions/10195812

我知道有許多單位因為種種原因,一時無法升級到 MySQL 8 ,
而且 MySQL 8 要做 Path Enumerations也要自己 Hardcode.

換成 Postgresql 呢? 換資料庫可是件大工程.

所以我想將使用 Postgresql 使用 fdw 的功能,將 Postgresql 怎樣連接 MySQL ,
並將 Adjacency List 型態的資料,使用之前介紹過的 ltree, 將資料變成 
Path Enumerations 與 Adjacency List 共存,甚至回寫到 MySQL 的一整個
過程,與大家分享.

階層式資料的處理,算是難度較高,也是一些ERP的重要核心,例如 BOM 展開,工序,組織,
物料關係等等.

來開始愛與勇氣的冒險吧.
--
先來看昨天的邦友的資料. https://ithelp.ithome.com.tw/questions/10195812

我們在 Postgresql 這邊建立一個鏡像的 table.過程如下:

# in PostgreSQL
# 先建立一個 連線的 server , 取名為 mysql_server
create server mysql_server
foreign data wrapper mysql_fdw
options (host '127.0.0.1', port '3306');

# 建立一個 mapping user, for 現在登入的 pg user
# 密碼以及 mysql 的 user, 要記得置換.
create user mapping for miku
server mysql_server
options (username 'miku1', password 'your$passwd');

--
# 接著使用 Postgresql 特殊的 import foreign schema
# 來建立對應的 table.
# 一般是用來匯入整個 schema , 有選項可以指定 table.
# 所以這次只匯入 ithelp191024

import foreign schema miku1
limit to (ithelp191024)
from server mysql_server
into miku;

[miku]# \dE
                List of relations
+--------+--------------+---------------+-------+
| Schema |     Name     |     Type      | Owner |
+--------+--------------+---------------+-------+
| miku   | ithelp191024 | foreign table | miku  |
+--------+--------------+---------------+-------+

[miku]# \dS ithelp191024
                  Foreign table "miku.ithelp191024"
+-----------+---------+-----------+----------+---------+-------------+
|  Column   |  Type   | Collation | Nullable | Default | FDW options |
+-----------+---------+-----------+----------+---------+-------------+
| id        | integer |           | not null |         |             |
| parent_id | integer |           |          |         |             |
+-----------+---------+-----------+----------+---------+-------------+
Server: mysql_server
FDW options: (dbname 'miku1', table_name 'ithelp191024')

select *
  from ithelp191024;

+----+-----------+
| id | parent_id |
+----+-----------+
|  1 |         3 |
|  2 |         3 |
...
| 26 |         ¤ |
+----+-----------+
(15 rows)

# 這樣不用手動建立 mapping table, import foreign schema 
# 幫我們建立好了.很方便.
# 接著建立 Postgresql 這邊有 ltree 型態的 table

create table p191025a (
  id int primary key
, parentid int
, path ltree
);

create unique index
on p191025a
using btree(path);

create index
on p191025a
using gist(path);

create or replace function p191025a_get_path(id int)
returns ltree as
$code$
select case when p.parentid is null 
        then p.id::text::ltree
        else p191025a_get_path(p.parentid) || p.id::text
       end
  from p191025a p
 where p.id = $1;
$code$
language sql;

create or replace function trig_upin_p191025a()
returns trigger as
$code$
begin
    if TG_OP = 'UPDATE' then
        if (coalesce(old.parentid, 0) != coalesce(new.parentid, 0)
             or new.id != old.id) then
            update p191025a
               set path = p191025a_get_path(id)
             where old.path @> p191025a.path;
        end if;
    elsif TG_OP = 'INSERT' then
        update p191025a
           set path = p191025a_get_path(new.id)
         where p191025a.id = new.id;
    end if;
    return new;
end
$code$
language plpgsql volatile;
create trigger trig_upin_p191025a
after insert or update of id, parentid
on p191025a for each row
execute procedure trig_upin_p191025a();

-----
# 建立好 trigger 後,我們直接透過 CTE 將 MySQL 的 資料
# insert 到 p191025a
--
insert into p191025a(id, parentid)
with recursive tree as (
select id
     , parent_id
  from ithelp191024
 where parent_id is null
 union all
select i.id
     , i.parent_id
  from ithelp191024 i
 inner join tree t
    on (i.parent_id = t.id)
)
select id, parent_id
  from tree;

select *
  from p191025a;

+----+----------+-------------+
| id | parentid |    path     |
+----+----------+-------------+
| 26 |        ¤ | 26          |
| 20 |       26 | 26.20       |
| 10 |       26 | 26.10       |
| 37 |       26 | 26.37       |
| 19 |       10 | 26.10.19    |
|  5 |       10 | 26.10.5     |
| 22 |       20 | 26.20.22    |
| 45 |       37 | 26.37.45    |
| 36 |       37 | 26.37.36    |
|  3 |        5 | 26.10.5.3   |
|  9 |        5 | 26.10.5.9   |
| 17 |       19 | 26.10.19.17 |
| 21 |       19 | 26.10.19.21 |
|  1 |        3 | 26.10.5.3.1 |
|  2 |        3 | 26.10.5.3.2 |
+----+----------+-------------+
(15 rows)

---
# 很優雅的顯示出來路徑了.
# 接著我們要把路徑資料轉給 MySQL,讓 MySQL 那邊也能方便使用,無需一個一個生成.
# 轉回去之前先在 MySQL 建立新的 table.

-- in MySQL
create table ithelp191025 (
  id int unsigned NOT NULL primary key
, parent_id int unsigned DEFAULT NULL
, path text
);

# 接著在 Postgresql 建立 mapping table 
-- in Postgresql
import foreign schema miku1
limit to (ithelp191025)
from server mysql_server
into miku;

# 將 p191025a 的資料 insert 給 ithelp191025
# 注意到將 ltree 轉型為 text
-- in Postgresql
insert into ithelp191025 (id, parent_id, path)
select id
     , parentid
     , path::text
  from p191025a;
  
# 注意, 最好做 commit 確保成果.

這時候在 MySQL 的 ithelp191025 已經有了 path 的資料了.
也就是說可以透過 fdw , 直接更新 MySQL 的資料.

https://ithelp.ithome.com.tw/upload/images/20191025/20050647hC8ykfLBMX.png

上面在 Postgresql 的 table , 我們使用了 trigger, 此
trigger 會遞迴呼叫 function. 然後我們再透過 CTE 遞迴
insert 到 table, 此時 trigger 會產生 path.也就是有
兩組遞迴;也許有人會擔心執行效能的問題.

先來看這道 SQL Command 及其執行結果.

with recursive tree as (
select id
     , parent_id
     , array[]::int[] as path
     , id::text as path2
  from ithelp191024
 where parent_id is null
 union all
select i.id
     , i.parent_id
     , path || i.id
     , path2 || '.' || i.id::text
  from ithelp191024 i
 inner join tree t
    on (i.parent_id = t.id)
)
select *
  from tree;
+----+-----------+------------+-------------+
| id | parent_id |    path    |    path2    |
+----+-----------+------------+-------------+
| 26 |         ¤ | {}         | 26          |
| 20 |        26 | {20}       | 26.20       |
| 10 |        26 | {10}       | 26.10       |
| 37 |        26 | {37}       | 26.37       |
| 19 |        10 | {10,19}    | 26.10.19    |
|  5 |        10 | {10,5}     | 26.10.5     |
| 22 |        20 | {20,22}    | 26.20.22    |
| 45 |        37 | {37,45}    | 26.37.45    |
| 36 |        37 | {37,36}    | 26.37.36    |
|  3 |         5 | {10,5,3}   | 26.10.5.3   |
|  9 |         5 | {10,5,9}   | 26.10.5.9   |
| 17 |        19 | {10,19,17} | 26.10.19.17 |
| 21 |        19 | {10,19,21} | 26.10.19.21 |
|  1 |         3 | {10,5,3,1} | 26.10.5.3.1 |
|  2 |         3 | {10,5,3,2} | 26.10.5.3.2 |
+----+-----------+------------+-------------+
(15 rows)

當 table 已經有 id, parent_id ,卻無 path 時,
可以使用 CTE 方式批次產生,此時是可以將 trigger 改成
在 insert 時不觸發.
trigger 主要是確保,更新時,能夠保持其一致性.
當批次更新完成後,可以改回 insert 跟 update 時均觸發.
這樣輸入更新都能確保正確性,平時輸入的語法也單純.


上一篇
我的Postgresql 陪我聽 500 Miles
系列文
以Postgresql為主,聊聊資料庫.31

1 則留言

0
ckp6250
iT邦新手 3 級 ‧ 2019-10-31 11:24:53

  一向都使用 Mysql,沒有接觸 Postgresql , 不知道它有這麼多強大的功能。

  看完這系列文章,發現它的確有很多【特異功能】可以簡便地解決 mysql 要花費大量精神才能得到相同的結果,比如在資料型態方面,多采多姿,遠比 mysql 完整而豐富。

受教了,感恩。

兩者定位不同
PostgreSQL: The World's Most Advanced Open Source Relational Database

MySQL: The World’s Most Popular Open Source Database

我要留言

立即登入留言