【问题标题】:Within a trigger function, how to get which fields are being updated在触发函数中,如何获取正在更新的字段
【发布时间】:2012-01-06 14:47:24
【问题描述】:

这可能吗?我有兴趣找出在 UPDATE 请求中指定了哪些列,而不管正在发送的新值可能是也可能不是已经存储在数据库中的值。

我想这样做的原因是因为我们有一个可以接收来自多个来源的更新的表。以前,我们没有记录更新的来源。现在该表存储了哪个源执行了最近的更新。我们可以更改一些来源以发送标识符,但这并不是所有事情的选择。所以我希望能够识别UPDATE 请求何时没有标识符,以便我可以替换为默认值。

【问题讨论】:

    标签: sql postgresql database-design triggers sql-update


    【解决方案1】:

    另一种方法是利用 PostgreSQL 最新版本中的 JSON/JSONB 函数。它的优点是可以处理任何可以转换为 JSON 对象(行或任何其他结构化数据)的东西,而且您甚至不需要知道记录类型。

    要找出任何两行/记录之间的差异,您可以使用这个小技巧:

    SELECT pre.key AS columname, pre.value AS prevalue, post.value AS postvalue
    FROM jsonb_each(to_jsonb(OLD)) AS pre
    CROSS JOIN jsonb_each(to_jsonb(NEW)) AS post
    WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value
    

    其中OLDNEW 是在触发器函数中发现的内置记录,分别表示更改记录的前后状态。请注意,我使用了表别名 prepost 而不是 oldnew 以避免与 OLD 和 NEW 内置对象发生冲突。还要注意使用IS DISTINCT FROM 而不是简单的!=<> 来适当地处理NULL 值。

    当然,这也适用于任何 ROW 构造函数,例如 ROW(1,2,3,...) 或其简写 (1,2,3,...)。它也适用于任何两个具有相同键的 JSONB 对象。

    例如,考虑一个有两行的示例(为了示例的目的,已经转换为 JSONB):

    SELECT pre.key AS columname, pre.value AS prevalue, post.value AS postvalue
    FROM jsonb_each('{"col1": "same", "col2": "prediff", "col3": 1, "col4": false}') AS pre
    CROSS JOIN jsonb_each('{"col1": "same", "col2": "postdiff", "col3": 1, "col4": true}') AS post
    WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value
    

    查询将显示已更改值的列:

     columname | prevalue  | postvalue
    -----------+-----------+------------
     col2      | "prediff" | "postdiff"
     col4      | false     | true
    

    这种方法很酷的一点是,按列过滤很简单。例如,假设您只想检测列col1col2 中的变化:

    SELECT pre.key AS columname, pre.value AS prevalue, post.value AS postvalue
    FROM jsonb_each('{"col1": "same", "col2": "prediff", "col3": 1, "col4": false}') AS pre
    CROSS JOIN jsonb_each('{"col1": "same", "col2": "postdiff", "col3": 1, "col4": true}') AS post
    WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value
    AND pre.key IN ('col1', 'col2')
    

    即使 col3 的值已更改,新结果也会从结果中排除:

     columname | prevalue  | postvalue
    -----------+-----------+------------
     col2      | "prediff" | "postdiff"
    

    很容易看出这种方法可以如何以多种方式进行扩展。例如,假设您想在某些列被更新时抛出异常。您可以通过通用触发功能来实现这一点,即可以应用于任何/所有表格的触发功能,而无需知道表格类型:

    CREATE OR REPLACE FUNCTION yourschema.yourtriggerfunction()
    RETURNS TRIGGER AS
    $$
    DECLARE
        immutable_cols TEXT[] := ARRAY['createdon', 'createdby'];
    BEGIN
    
        IF TG_OP = 'UPDATE' AND EXISTS(
            SELECT 1
            FROM jsonb_each(to_jsonb(OLD)) AS pre, jsonb_each(to_jsonb(NEW)) AS post
            WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value
            AND pre.key = ANY(immutable_cols)
        ) THEN
            RAISE EXCEPTION 'Error 12345 updating table %.%. Cannot alter these immutable cols: %.',
                TG_TABLE_SCHEMA, TG_TABLE_NAME, immutable_cols;
        END IF;
    
    END
    $$
    LANGUAGE plpgsql VOLATILE
    

    然后,您可以将上述触发器函数注册到您想要通过以下方式控制的任何和所有表中:

    CREATE TRIGGER yourtiggername
    BEFORE UPDATE ON yourschema.yourtable
    FOR EACH ROW EXECUTE PROCEDURE yourschema.yourtriggerfunction();
    

    【讨论】:

    • 太棒了。我在 SQLite 上也看到过类似的东西,使用 JSON1 扩展,虽然不太方便,因为如果我没记错的话,没有与 to_jsonb(OLD) 等效的东西。
    【解决方案2】:

    如果“来源”没有“发送标识符”,则该列将保持不变。然后您就无法检测到当前UPDATE 是由与上一个相同的源完成的,还是由根本没有更改列的源完成的。换句话说:这不能正常工作。

    如果任何session information function 都可以识别“来源”,则可以使用它。喜欢:

    NEW.column = session_user;
    

    每次更新都是无条件的。

    一般解决方案

    我找到了解决原始问题的方法。该列将在 any 更新中设置为默认值,其中列未更新(不在UPDATESET 列表中)。

    关键元素是 PostgreSQL 9.0 中引入的 per-column trigger - 使用 UPDATE OF column_name 子句的特定于列的触发器。

    仅当列出的列中至少有一个列被触发时,触发器才会触发 提到作为UPDATE 命令的目标。

    这是我发现的唯一简单的方法来区分列是否使用与旧值相同的新值更新,而不是根本没有更新。

    一个可以也解析current_query()返回的文本。但这似乎很棘手且不可靠。

    触发函数

    我假设一列source 定义为NOT NULL

    第 1 步:source 设置为NULL(如果未更改):

    CREATE OR REPLACE FUNCTION trg_tbl_upbef_step1()
      RETURNS trigger
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       IF NEW.source = OLD.source THEN
          NEW.source := NULL;      -- "impossible" value (source is NOT NULL)
       END IF;
    
       RETURN NEW;
    END
    $func$;
    

    第 2 步:恢复为旧值。 只有在实际更新了值时才会触发触发器(见下文):

    CREATE OR REPLACE FUNCTION trg_tbl_upbef_step2()
      RETURNS trigger
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       IF NEW.source IS NULL THEN
          NEW.source := OLD.source;
       END IF;
    
       RETURN NEW;
    END
    $func$;
    

    第 3 步:现在我们可以识别缺少的更新并改为设置默认值:

    CREATE OR REPLACE FUNCTION trg_tbl_upbef_step3()
      RETURNS trigger
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       IF NEW.source IS NULL THEN
          NEW.source := 'UPDATE default source';  -- optionally same as column default
       END IF;
    
       RETURN NEW;
    END
    $func$;
    

    触发器

    Step 2 的触发器按列触发!

    CREATE TRIGGER upbef_step1
      BEFORE UPDATE ON tbl
      FOR EACH ROW
      EXECUTE PROCEDURE trg_tbl_upbef_step1();
    
    CREATE TRIGGER upbef_step2
      BEFORE UPDATE OF source ON tbl             -- key element!
      FOR EACH ROW
      EXECUTE PROCEDURE trg_tbl_upbef_step2();
        
    CREATE TRIGGER upbef_step3
      BEFORE UPDATE ON tbl
      FOR EACH ROW
      EXECUTE PROCEDURE trg_tbl_upbef_step3();

    db小提琴here

    触发器名称是相关的,因为它们按字母顺序触发(全部为BEFORE UPDATE)!

    可以使用“每列触发器”之类的方法或任何其他方式来检查触发器中UPDATE 的目标列表,从而简化该过程。但我目前看不到这方面的处理方法(自 Postgres 14 起未更改)。

    如果source 可以是NULL,则使用任何其他“不可能”的中间值并在触发函数1 中另外检查NULL

    IF OLD.source IS NOT DISTINCT FROM NEW.source THEN
        NEW.source := '#impossible_value#';
    END IF;
    

    相应地调整其余部分。

    【讨论】:

    • 啊,我刚试过,你是对的。我们的来源是许多不同的端点,可供许多不同的用户使用。所以我们感兴趣的标识符是更新来自哪个端点和来自哪个用户的组合。所以更新可能看起来像'UPDATE table SET source = source_id,user = user_id,data = new_data'或只是'UPDATE table set data = new_data'。我想作为一个黑客,我可以检查 current_query() 中的文本,这些文本会指示那些识别列是否被设置。
    • @Erwin:我认为您的回答可以完成工作 :-) 只是为了兴趣(即我自己没有进行研究):您认为这可能是规则系统的工作吗?
    • @A.H.:同样的限制也适用于规则:rule condition, only NEW and OLD are visible。我看不出有什么办法可以区分使用相同值更新列的情况与根本不更新列的情况。你呢?
    • 啊哈!我没有意识到每列触发器的存在!很聪明!
    • 我很遗憾我只有一个赞成这个答案。
    【解决方案3】:

    在 plpgsql 中,您可以在触发函数中执行以下操作:

    IF NEW.column IS NULL THEN
      NEW.column = 'default value';
    END IF;
    

    【讨论】:

    • 如此简单!自然,我可以在阅读回复后在文档中找到这个答案...
    • 设置后真的会删除一个值吗? IE。如果表格行包含column 的某些值,那么您的代码仅在语句类似于UPDATE table SET column = null, data_col=... 时才有效,但这需要采用提问者无权访问的代码。
    • 它不检查旧数据,但如果需要,这是可能的。只需添加一些代码。
    • @FrankHeikens:您如何区分与上次更新相同来源的更新和根本不更改列的更新?
    • @A.H:你可能对我找到的解决方案感兴趣。
    【解决方案4】:

    我几乎很自然地获得了类似问题的另一种解决方案,因为我的表包含一个语义为“上次更新时间戳”的列(我们称之为 UPDT)。

    因此,我决定一次在任何更新中包含 source 和 UPDT 的新值(或一个都不包含)。由于 UPDT 旨在在每次更新时进行更改,因此可以使用条件new.UPDT = old.UPDT 推断当前更新未指定源并替换默认源。

    如果一个人的表中已经有“上次更新时间戳”列,则此解决方案将比创建三个触发器更简单。不确定在不需要时创建 UPDT 是否更好。如果更新如此频繁以至于存在时间戳相似的风险,则可以使用排序器代替时间戳。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-18
      • 2012-08-01
      • 2016-01-03
      • 1970-01-01
      • 2015-03-08
      相关资源
      最近更新 更多