postgresql 更新具有触发器非空约束的行时出错

4nkexdtk  于 2023-06-05  发布在  PostgreSQL
关注(0)|答案(1)|浏览(112)

我正在使用触发器更新一个名为tb_sign_employees的表,其结构如下:

sign_id                 INT NOT NULL,
employee_id             INT NOT NULL,
sign_term_in                CHARACTER(3) NOT NULL,
sign_data_in                TIMESTAMP NOT NULL,
sign_term_out               CHARACTER(3),
sign_data_out               TIMESTAMP,
    sign_time_minutes           INT,

主键是sign_id。我需要使用触发器tg_sign_inserted(),所以当我插入employee_id和sign_term_in时,它会自动插入一个新的sign_id,它是1加上最大的sign_id和sign_data_in,这是当前的时间戳,剩下的为null。然后,如果我插入employee_id和sign_term_out,它应该用我输入的那个更新sign_term_out,用当前时间戳更新sign_data_out,并将sign_time_minutes作为sign_data_out减去sign_data_in分钟。此外,如果您之前已经执行过但未更新值,则无法插入,如果您签入但未 checkout ,则无法再次签入。如果你已经 checkout 了,就不可能 checkout ,你不能在插入时使用sign_tern_in和sign_term_out。我试过:

CREATE OR REPLACE FUNCTION erp.fn_check_sign_inserted_values(new_sign erp.tb_sign_employees)
RETURNS BOOLEAN AS $$
BEGIN
    IF new_sign.sign_term_in IS NULL AND new_sign.sign_term_out IS NULL THEN
        RAISE EXCEPTION 'Cal informar de sign_term_in o sign_term_out';
    END IF;

    IF new_sign.sign_term_in IS NOT NULL AND new_sign.sign_term_out IS NOT NULL THEN
        RAISE EXCEPTION 'Cal informar de sign_term_in o sign_term_out, no els dos';
    END IF;

    IF new_sign.sign_term_in IS NOT NULL THEN
        IF new_sign.employee_id IS NULL THEN
            RAISE EXCEPTION 'Cal informar del id del empleat';
        END IF;

        IF EXISTS (
            SELECT 1
            FROM erp.tb_sign_employees
            WHERE employee_id = new_sign.employee_id
            AND sign_term_out IS NULL
        ) THEN
            RAISE EXCEPTION E'Ja hi ha un registre d''entrada sense sortida';
        END IF;
    END IF;

    IF new_sign.sign_term_out IS NOT NULL THEN
        IF new_sign.employee_id IS NULL THEN
            RAISE EXCEPTION 'Cal informar del id del empleat';
        END IF;

        IF NOT EXISTS (
            SELECT 1
            FROM erp.tb_sign_employees
            WHERE employee_id = new_sign.employee_id
            AND sign_term_out IS NULL
        ) THEN
            RAISE EXCEPTION E'Ja hi ha un registre d''entrada sense sortida';
        END IF;
    END IF;

    RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION erp.tg_sign_inserted()
RETURNS TRIGGER AS $$
BEGIN
    IF NOT erp.fn_check_sign_inserted_values(NEW) THEN
        RETURN NULL;
    END IF;

    IF NEW.sign_term_in IS NOT NULL THEN
        INSERT INTO erp.tb_sign_employees (sign_id, employee_id, sign_term_in, sign_data_in)
        SELECT COALESCE(MAX(sign_id), 0) + 1, NEW.employee_id, NEW.sign_term_in, current_timestamp
        FROM erp.tb_sign_employees;
    ELSIF NEW.sign_term_out IS NOT NULL THEN
        UPDATE erp.tb_sign_employees
        SET sign_term_out = NEW.sign_term_out,
            sign_data_out = current_timestamp,
            sign_time_minutes = extract(epoch from current_timestamp - sign_data_in) / 60
        WHERE employee_id = NEW.employee_id
        AND sign_term_out IS NULL
        AND sign_term_in IS NOT NULL; -- Agregamos esta condición para actualizar solo las filas con sign_term_in ya definido.
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tg_insert_sign_employee
BEFORE INSERT ON erp.tb_sign_employees
FOR EACH ROW
EXECUTE FUNCTION erp.tg_sign_inserted()

当我用途:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

它的工作原理只是文件,但如果然后我用途:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal1')

它并没有填充由前面的命令生成的行,而是尝试创建一个新行,如下所示:(null,1,null,null,EP,null,null)导致违反列sign_id中的非空约束。它不更新列的事实是一个错误,但不是唯一的错误,因为试图创建的列包含null,而不是在sing_data_out中插入当前时间戳。我看不到错误。为了给予你一个它应该如何表现的例子,假设最大的sign_id是1。然后使用日期2023-03-06在9点钟:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

应创建以下行:

2, 1, Terminal1, 2023-03-06 09:14:00, null, null, null

如果我在同一天12点执行了以下操作:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal2')
2, 1, Terminal1, 2023-03-06 09:00:00, Terminal2, 2023-03-06 12:00:00, 180

其中180是12点和9点之间的分钟差。先谢谢你了。

ktca8awb

ktca8awb1#

你的扳机有几个大问题。如上所述,它不会像声明的那样插入初始行。触发器在insert上触发到表erp.tb_sign_employees。然后在触发器中,它尝试执行相同的操作:

IF NEW.sign_term_in IS NOT NULL THEN
        INSERT INTO erp.tb_sign_employees (sign_id, employee_id, sign_term_in, sign_data_in)
        SELECT COALESCE(MAX(sign_id), 0) + 1, NEW.employee_id, NEW.sign_term_in, current_timestamp
        FROM erp.tb_sign_employees;

结果,触发器再次触发,并且由于列NEW.sign_term_in仍然为空,因此触发器再次触发。它创建了一个无限循环,最终导致异常:
SQL错误[54001]:错误:超出堆栈深度限制...;
不应在触发器中发出与最初触发触发器的语句相同的DML语句。在这种情况下,这是不必要的。您可以对列进行直接分配或选择进入,以便:

select coalesce(max(sign_id), 0) + 1
  into new.sign_id
  from tb_sign_employees;

现在我们来谈第二个问题。以上是非常糟糕的实践,因为在多用户环境中,MAX+1分配实际上是获得副本的保证。当两个用户尝试插入一行,并且第二个用户在第一个用户提交之前执行时,就会发生这种情况,因为每个用户都将获得相同的MAX值。
现在来谈谈实际上引发你的问题的问题。您声明 * 它不更新列是一个错误... *。这是错误的,它正确地更新。问题是你实际上把一个insert变成了一个update。但是,Postgres不会识别这种更改,因此当您在return new之后执行时,它会继续处理插入。由于列sign_in没有被赋值,也没有自动生成,所以它是NULL,因此您会得到空值异常,插入被中止。不幸的是,更新也被中止(回滚),所以它看起来还没有完成。正确的处理方法是只发出一个update而不是第二个insert。然而,周围有一个非常规的工作。更新后不发出return new,而是发出

return null;

返回null将终止对insert的进一步处理,但将保留更新的行。(行计数,如果taken将被关闭,则终止的插入不计为已处理的行。)考虑到所有这些,您的触发器函数变为:

create or replace function tg_sign_inserted()
  returns trigger 
 language plpgsql
as $$ 
begin

    if not fn_check_sign_inserted_values(new) then
        return null;
    end if;

    if new.sign_term_in is not null then
        select coalesce(max(sign_id), 0) + 1
          into new.sign_id
          from tb_sign_employees ;
        return new;
    
    elsif new.sign_term_out is not null then

        update tb_sign_employees
        set sign_term_out = new.sign_term_out,
            sign_data_out = current_timestamp,
            sign_time_minutes = extract(epoch from current_timestamp - sign_data_in) / 60
        where employee_id = new.employee_id
        and sign_term_out is null
        and sign_term_in is not null; -- agregamos esta condición para actualizar solo las filas con sign_term_in ya definido.
        
        return null; -- tell Postgres to stop Processing the Insert
    end if;

end;
$$;

参见demo here。注意:对于演示,我已经硬编码了您提到的时间戳,并更正了以下内容。
最后,即使没有上述问题,您的初始插入也会失败,原因还有两个,可能只是错别字。1.列sign_term_insign_term_out被定义为CHARACTER(3),但您希望插入值Terminal1Terminal2。这些值将导致异常,因为它们不适合分配的字段限制。2.列sign_data_in被定义为非空,但没有给出默认值,也没有在insert上引用。因此,它将引发空值异常。
您声明您的 * 命令是不修改结构 *。要插入指定的值,您必须**修改结构。当你和那些给你下命令的人讨论这个问题的时候,告诉他们MAX+1对于分配sign_id是多么的糟糕,并让他们也改变它。改变这两个,你最有可能可以消除触发所有在一起。

相关问题