postgresql 在触发器函数中,如何获取正在更新的字段

ocebsuys  于 2022-11-04  发布在  PostgreSQL
关注(0)|答案(4)|浏览(370)

这可能吗?我感兴趣的是找出在UPDATE请求中指定了哪些列,而不管正在发送的新值可能是数据库中已经存储的值,也可能不是。
我这样做的原因是因为我们有一个可以从多个源接收更新的表。以前,我们不记录更新来自哪个源。现在,表存储了哪个源执行了最近的更新。我们可以更改一些源以发送标识符,但这并不是万能的,所以我希望能够识别出UPDATE请求没有标识符的情况,这样我就可以用一个默认值来代替。

xlpyo6sf

xlpyo6sf1#

如果一个“源”没有“发送标识符”,那么该列将保持不变。这样你就无法检测当前的UPDATE是由与上一个相同的源完成的,还是由一个根本没有改变该列的源完成的。换句话说:这不能正常工作。
如果“源”可由任何会话信息函数识别,则可以使用该函数。

NEW.column = session_user;

无条件地进行每次更新。

一般解决方案

我找到了解决原来问题的办法。
如果该列不是UPDATE中的目标列(不在SET列表中),则将其设置为默认值。关键元素是PostgreSQL 9.0引入的每列触发器-使用UPDATE OF * column_name * 子句的特定于列的触发器。The manual
仅当列出的列中至少有一列被指定为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$;

触发器

  • 步骤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〈〉小提琴
    触发器名称是相关的,因为它们是按字母顺序触发的(全部为BEFORE UPDATE)!

这个过程可以用类似“per-not-column triggers”的方式来简化,或者用任何其他方法来检查触发器中UPDATE的target-list。但是我没有看到这个句柄,目前(Postgres 14没有改变)。
如果source可以是NULL,则使用任何其他“不可能”的中间值,并在触发函数1中额外检查NULL

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

对其余部分进行相应调整。

j2datikz

j2datikz2#

另一种方法是利用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是在触发器函数中找到的内置记录,分别表示已更改记录的pre和after状态。请注意,我使用了表别名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"

很容易看出这种方法可以通过多种方式进行扩展。例如,假设您希望在更新某些列时抛出异常。您可以使用universal触发器函数来实现这一点,也就是说,该函数可以应用于任何/所有表,而不必知道表类型:

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();
ssgvzors

ssgvzors3#

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

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

x6492ojm4#

我几乎很自然地得到了类似问题的另一个解决方案,因为我的表包含了一个语义为“上次更新时间戳”(我们称之为UPDT)的列。
因此,我决定在任何更新中一次性包含source和UPDT的新值(或者一个都不包含)。由于UPDT在每次更新时都会改变,因此使用这种策略可以使用条件new.UPDT = old.UPDT来推断当前更新没有指定源,并替换默认的源。
如果表中已经有了“上次更新时间戳”列,则此解决方案比创建三个触发器简单。如果不需要UPDT,则不确定创建UPDT是否是更好的主意。如果更新非常频繁,存在时间戳相似的风险,则可以使用序列器来代替时间戳。

相关问题