【问题标题】:How to programmatically check if row is deletable?如何以编程方式检查行是否可删除?
【发布时间】:2013-10-23 19:40:53
【问题描述】:

假设我们有一个像这样的 PostgreSQL 表:

CREATE TABLE master (
    id INT PRIMARY KEY,
    ...
);

以及许多其他用外键引用它的表:

CREATE TABLE other (
    id INT PRIMARY KEY,
    id_master INT NOT NULL,
    ...
    CONSTRAINT other_id_master_fkey FOREIGN KEY (id_master)
                                    REFERENCES master (id) ON DELETE RESTRICT
);

有没有办法检查(从触发器函数中)主行是否可删除而不实际尝试删除它?显而易见的方法是对所有引用表逐一执行 SELECT,但我想知道是否有更简单的方法。

原因我需要这个是因为我有一个包含分层数据的表,其中任何行都可以有子行,并且只有层次结构中最低的子行才能被其他表引用。所以当一行即将成为父行时,我需要检查它是否已经在任何地方被引用。如果是,则不能成为父行,拒绝插入新的子行。

【问题讨论】:

    标签: sql postgresql database-design plpgsql sql-delete


    【解决方案1】:

    您可以尝试删除该行并回滚效果。您不希望在触发器函数中这样做,因为任何异常都会取消对数据库的所有持久更改。 The manual:

    EXCEPTION 子句捕捉到错误时, PL/pgSQL 函数保持错误发生时的状态,但是 块内对持久数据库状态的所有更改都将回滚

    粗体强调我的。

    但您可以将其包装到单独的块或单独的 plpgsql 函数中并在那里捕获异常以防止对主(触发器)函数的影响。

    CREATE OR REPLACE FUNCTION f_can_del(_id int)
      RETURNS boolean AS 
    $func$
    BEGIN
       DELETE FROM master WHERE master_id = _id; -- DELETE is always rolled back
    
       IF NOT FOUND THEN
          RETURN NULL;                        -- ID not found, return NULL
       END IF;
    
       RAISE SQLSTATE 'MYERR';                -- If DELETE, raise custom exception
    
       EXCEPTION
       WHEN FOREIGN_KEY_VIOLATION THEN
          RETURN FALSE;
       WHEN SQLSTATE 'MYERR' THEN
          RETURN TRUE;
       -- other exceptions are propagated as usual
    END  
    $func$ LANGUAGE plpgsql;
    

    这会返回TRUE/FALSE/NULL,表示该行可以删除/不能删除/不存在。

    db小提琴here
    sqlfiddle

    可以轻松地使这个函数动态化以测试任何表/列/值。

    由于PostgreSQL 9.2,您还可以报告哪个表被阻塞。
    PostgreSQL 9.3 或更高版本提供更详细的信息。

    任意表、列和类型的通用函数

    为什么您在 cmets 中发布的 the attempt on a dynamic function 失败了? This quote from the manual应该给个线索:

    特别注意EXECUTE 会改变GET DIAGNOSTICS 的输出,但不会改变FOUND

    它适用于 GET DIAGNOSTICS

    CREATE OR REPLACE FUNCTION f_can_del(_tbl regclass, _col text, _id int)
      RETURNS boolean AS 
    $func$
    DECLARE
       _ct int;                              -- to receive count of deleted rows
    BEGIN
       EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
          USING _id;                         -- exception if other rows depend
    
       GET DIAGNOSTICS _ct = ROW_COUNT;
    
       IF _ct > 0 THEN
          RAISE SQLSTATE 'MYERR';            -- If DELETE, raise custom exception
       ELSE
          RETURN NULL;                       -- ID not found, return NULL
       END IF;
    
       EXCEPTION
       WHEN FOREIGN_KEY_VIOLATION THEN
          RETURN FALSE;
       WHEN SQLSTATE 'MYERR' THEN
          RETURN TRUE;
       -- other exceptions are propagated as usual
    END  
    $func$ LANGUAGE plpgsql;
    

    db小提琴here
    sqlfiddle

    在此过程中,我将其完全动态化,包括列的数据类型(当然,它必须与给定的列匹配)。我为此目的使用polymorphic type anyelement。见:

    我还使用format()regclass 类型的参数来防范SQLi。见:

    【讨论】:

    • 另一种方法是从目录表中动态创建 SELECT 语句。考虑相关答案herehere
    • @NiksaBaldun:由于DELETE 永远不会通过,它应该适合requirements for the STABLE declaration。但我看不出你会从中获得什么。基本上它告诉查询规划器,如果一个函数被多次调用,它可以在单个查询中重用结果。这种类型的函数可能只调用一次。如有更多问题,请开始另一个问题。
    • 显然,即使所有更改都会回滚,也不能声明为 STABLE。 PostgreSQL 似乎不允许在 STABLE 函数中使用任何 UPDATE 或 DELETE 语句。
    【解决方案2】:

    您也可以使用 Procedure 来做到这一点。

    CREATE OR REPLACE procedure p_delable(_tbl text, _col text, _id int)
     AS  $$
    DECLARE
       _ct bigint; 
       _exists boolean;                             -- to receive count of deleted rows
    BEGIN
        _exists := (SELECT EXISTS ( SELECT FROM information_schema.tables 
            WHERE  table_schema = 'public' AND    table_name   = $1 ));
        IF _exists THEN
            EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
                USING _id;                         -- exception if other rows depend
            GET DIAGNOSTICS _ct = ROW_COUNT;
    
            IF _ct > 0 THEN
                RAISE SQLSTATE 'MYERR';            -- If DELETE, raise custom exception
            ELSE
                RAISE NOTICE 'no records found. no records will be deleted';
            END IF;
        ELSE
            raise notice 'Input text is invalid table name.';
        END IF;
       EXCEPTION
       WHEN undefined_column then
          raise notice 'Input text is invalid column name.';
       WHEN undefined_table then
          raise notice 'Input text is invalid table name.';
       WHEN FOREIGN_KEY_VIOLATION THEN
          RAISE NOTICE 'foreign key violation, cannot be deleted.';
       WHEN SQLSTATE 'MYERR' THEN
          RAISE NOTICE 'rows % found and can be deleted.', _ct;
    END  
    $$ LANGUAGE plpgsql; 
    

    你可以调用它,也可以验证你的输入。

    call p_delable('parent_tree', 'parent_id',30);
    

    将获得:

    NOTICE:  no records found. no records will be deleted
    

    让我们尝试一个实际存在的行。

    call p_delable('parent_tree', 'parent_id',3);
    

    它会返回

    NOTICE:  rows 1 found and can be deleted.
    

    它还可以检查您的输入表名是否存在于public 架构中。

    call p_delable('parent_tre', 'parent_id',3);
    

    它会通知你:

    NOTICE:  Input text is invalid table name.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多