18

我想更改一个主键和所有引用该值的表行。

# table master
master_id|name
===============
foo|bar

# table detail
detail_id|master_id|name
========================
1234|foo|blu

如果我给出一个脚本或函数

 table=master, value-old=foo, value-new=abc

我想创建一个 SQL 片段,对引用表“master”的所有表执行更新:

update detail set master_id=value-new where master_id=value-new;
.....

在自省的帮助下,这应该是可能的。

我使用postgres。

更新

问题是,有许多表具有表“主”的外键。我想要一种方法来自动更新所有具有主表外键的表。

4

4 回答 4

25

到目前为止,处理主键更改的最简单方法是ALTER引用外键约束ON UPDATE CASCADE

然后,您可以自由更新主键值,并且更改将级联到子表。由于所有随机 I/O,这可能是一个非常缓慢的过程,但它会起作用。

在此过程中,您确实需要注意不要违反主键列的唯一性约束。

一个更复杂但更快的方法是UNIQUE为新 PK 添加一个新列,填充它,向所有指向新 PK 的引用表添加新列,删除旧 FK 约束和列,然后最后删除旧 PK。

于 2013-08-16T11:04:33.047 回答
12

如果您需要更改 PK,您可以使用DEFFERED CONSTRAINTS

SET CONSTRAINTS 设置当前事务中约束检查的行为。在每个语句的末尾检查 IMMEDIATE 约束。在事务提交之前不会检查 DEFERRED 约束。每个约束都有自己的 IMMEDIATE 或 DEFERRED 模式。

资料准备:

CREATE TABLE master(master_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10));
INSERT INTO master(master_id, name) VALUES ('foo', 'bar');

CREATE TABLE detail(detail_id INT PRIMARY KEY, master_id VARCHAR(10)
   ,name VARCHAR(10)
   ,CONSTRAINT  fk_det_mas FOREIGN KEY (master_id) REFERENCES master(master_id));

INSERT INTO detail(detail_id, master_id, name) VALUES (1234,'foo','blu');

在正常情况下,如果您尝试更改主详细信息,您最终会出现错误:

update detail set master_id='foo2' where master_id='foo';
-- ERROR:  insert or update on table "detail" violates foreign key 
-- constraint "fk_det_mas"
-- DETAIL:  Key (master_id)=(foo2) is not present in table "master"

update master set master_id='foo2' where master_id='foo';
-- ERROR:  update or delete on table "master" violates foreign key
-- constraint "fk_det_mas" on table "detail"
-- DETAIL:  Key (master_id)=(foo) is still referenced from table "detail".

但是如果你把FK分辨率改成defered,就没有问题了:

ALTER TABLE detail DROP CONSTRAINT fk_det_mas ;
ALTER TABLE detail ADD CONSTRAINT fk_det_mas FOREIGN KEY (master_id) 
REFERENCES master(master_id) DEFERRABLE;

BEGIN TRANSACTION;
SET CONSTRAINTS ALL DEFERRED;
UPDATE master set master_id='foo2' where master_id = 'foo';
UPDATE detail set master_id='foo2' where master_id = 'foo';
COMMIT;

DBFiddle 演示

请注意,您可以在事务中做很多事情,但在COMMIT 所有参照完整性检查期间都必须进行。

编辑

如果你想自动化这个过程,你可以使用动态 SQL 和元数据表。这里是一个 FK 列的概念证明:

CREATE TABLE master(master_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10));
INSERT INTO master(master_id, name)
VALUES ('foo', 'bar');

CREATE TABLE detail(detail_id INT PRIMARY KEY, master_id VARCHAR(10),
   name VARCHAR(10)
  ,CONSTRAINT  fk_det_mas FOREIGN KEY (master_id) 
   REFERENCES master(master_id)DEFERRABLE ) ;
INSERT INTO detail(detail_id, master_id, name) VALUES (1234,'foo','blu');

CREATE TABLE detail_second(detail_id INT PRIMARY KEY, name VARCHAR(10),
   master_id_second_name VARCHAR(10)
  ,CONSTRAINT  fk_det_mas_2 FOREIGN KEY (master_id_second_name) 
   REFERENCES master(master_id)DEFERRABLE ) ;
INSERT INTO detail_second(detail_id, master_id_second_name, name) 
VALUES (1234,'foo','blu');

和代码:

BEGIN TRANSACTION;
SET CONSTRAINTS ALL DEFERRED;
DO $$
DECLARE
   old_pk TEXT = 'foo';
   new_pk TEXT = 'foo2';
   table_name TEXT = 'master';
BEGIN
-- update childs
EXECUTE (select 
         string_agg(FORMAT('UPDATE %s SET %s = ''%s'' WHERE %s =''%s'' ;'
            ,c.relname,pa.attname, new_pk,pa.attname, old_pk),CHR(13)) AS sql
         from  pg_constraint pc
         join pg_class c on pc.conrelid = c.oid
         join pg_attribute pa ON pc.conkey[1] = pa.attnum 
          and pa.attrelid = pc.conrelid
         join pg_attribute pa2 ON pc.confkey[1] = pa2.attnum 
          and pa2.attrelid = table_name::regclass
         where pc.contype = 'f');

-- update parent        
EXECUTE ( SELECT FORMAT('UPDATE %s SET %s = ''%s'' WHERE %s =''%s'';'
         ,c.relname,pa.attname, new_pk,pa.attname, old_pk)
 FROM pg_constraint pc
 join pg_class c on pc.conrelid = c.oid
 join pg_attribute pa ON pc.conkey[1] = pa.attnum 
  and pa.attrelid = pc.conrelid
 WHERE pc.contype IN ('p','u')
   AND conrelid = table_name::regclass
);       
         
END
$$;
COMMIT;

DBFiddle 演示 2

编辑2:

我试过了,但它不起作用。如果脚本可以显示 SQL,那就太好了。这就够了。在查看生成的 SQL 之后,如果 psql -f 我可以执行它

你试过吗?它对我不起作用。

是的,我试过了。只需检查上面的现场演示链接。我准备了带有更多调试信息的相同演示:

  • 之前的值
  • 执行的 SQL
  • 之后的值

请确保将 FK 定义为 DEFFERED。

带有调试信息的 DBFiddle 2

最后编辑

然后我想查看 sql 而不是执行它。我从您的小提琴中删除了“执行”,但随后出现错误。见:http ://dbfiddle.uk/?rdbms=postgres_10&fiddle=b9431c8608e54b4c42b5dbd145aa1458

如果您只想获取 SQL 代码,您可以创建函数:

CREATE FUNCTION generate_update_sql(table_name VARCHAR(100), old_pk VARCHAR(100), new_pk VARCHAR(100))
RETURNS TEXT 
AS 
$$
BEGIN
RETURN 
-- update childs
(SELECT 
         string_agg(FORMAT('UPDATE %s SET %s = ''%s'' WHERE %s =''%s'' ;',  c.relname,pa.attname, new_pk,pa.attname, old_pk),CHR(13)) AS sql
         FROM  pg_constraint pc
         JOIN pg_class c on pc.conrelid = c.oid
         JOIN pg_attribute pa ON pc.conkey[1] = pa.attnum and pa.attrelid = pc.conrelid
         JOIN pg_attribute pa2 ON pc.confkey[1] = pa2.attnum and pa2.attrelid = table_name::regclass
         WHERE pc.contype = 'f') || CHR(13) ||
-- update parent        
(SELECT FORMAT('UPDATE %s SET %s = ''%s'' WHERE %s =''%s'';',  c.relname,pa.attname, new_pk,pa.attname, old_pk)
 FROM pg_constraint pc
 JOIN pg_class c on pc.conrelid = c.oid
 JOIN pg_attribute pa ON pc.conkey[1] = pa.attnum and pa.attrelid = pc.conrelid
 WHERE pc.contype IN ('p','u')
   AND conrelid = table_name::regclass)
;       
END
$$ LANGUAGE  plpgsql;

和执行:

SELECT generate_update_sql('master', 'foo', 'foo');

UPDATE detail SET master_id = 'foo' WHERE master_id ='foo' ;
UPDATE detail_second SET master_id_second_name = 'foo' 
 WHERE master_id_second_name ='foo' ; 
UPDATE master SET master_id = 'foo' WHERE master_id ='foo';

DBFiddle 函数演示

当然还有改进的地方,例如处理标识符,如“名称中带有空格的表”等。

于 2018-03-13T20:42:16.173 回答
3

我找到了一个肮脏的解决方案:在psql命令\d master_table中显示相关信息。使用一些文本魔法,可以提取所需的信息:

echo "UPDATE master_table SET id='NEW' WHERE id='OLD';" > tmp/foreign-keys.txt

psql -c '\d master_table' | grep -P 'TABLE.*CONSTRAINT.*FOREIGN KEY'  \
                                 >> tmp/foreign-keys.txt

reprec '.*TABLE ("[^"]*") CONSTRAINT[^(]*\(([^)]*)\).*' \
        "UPDATE \1 set \2='NEW' WHERE \2='OLD';" \
         tmp/foreign-keys.txt 

psql -1 -f tmp/foreign-keys.txt 

结果:

UPDATE "master_table" SET id='NEW' WHERE id='OLD';
UPDATE "other_table" SET master_id='NEW' WHERE master_id='OLD';
...

但欢迎更好的解决方案。

于 2013-08-16T12:11:19.983 回答
2

我不认为你可以更新主键。一种可能的解决方法是您可以从表列中删除主键约束。然后更新列值。

更新主键可能会导致一些严重的问题。但如果你还想做。

请参考这个线程。(kevchadders 给出了解决方案。)

于 2013-08-16T10:56:04.607 回答